├── pyledger ├── __init__.py ├── pyledger_message.proto ├── server │ ├── __init__.py │ ├── status.py │ ├── auth.py │ ├── config.py │ ├── contract.py │ ├── ws.py │ ├── db.py │ └── handlers.py ├── client │ ├── __init__.py │ ├── lib.py │ ├── ws.py │ └── repl.py ├── verify.py └── pyledger_message_pb2.py ├── requirements.txt ├── docs ├── source │ ├── currency_example.rst │ ├── server.rst │ ├── distributed.rst │ ├── users.rst │ ├── chain.rst │ ├── conf.py │ ├── index.rst │ └── contract.rst ├── Makefile └── make.bat ├── examples ├── hello │ └── server.py ├── hello_counter │ └── server.py ├── hello_name │ └── server.py ├── hello_exception │ └── server.py └── currency │ └── server.py ├── .gitignore ├── tests ├── test_4_status.py ├── test_1_sessions.py ├── test_6_call.py ├── test_2_handlers.py ├── test_8_clientlib.py ├── test_5_handlers.py ├── test_7_contract_auth.py ├── test_0_users.py └── test_3_simple_contract.py ├── setup.py ├── README.rst └── LICENSE /pyledger/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | autobahn 2 | protobuf 3 | google 4 | sqlalchemy 5 | cryptography 6 | -------------------------------------------------------------------------------- /docs/source/currency_example.rst: -------------------------------------------------------------------------------- 1 | Example. A digital currency with permissions 2 | ============================================ 3 | 4 | This example makes use of all the features that have been commented so far. 5 | It implements a more or less correct cryptocurrency. 6 | 7 | .. literalinclude:: ../../examples/currency_auth/server.py 8 | 9 | -------------------------------------------------------------------------------- /docs/source/server.rst: -------------------------------------------------------------------------------- 1 | Options for running a ledger server 2 | =================================== 3 | 4 | When a server is built using :py: 5 | 6 | You can take a look at SQLAlchemy's 7 | `engine configuration `_ 8 | session to see how this option should be formatted. 9 | 10 | Docstrings cited in this section 11 | -------------------------------- 12 | 13 | .. autofunction:: pyledger.handlers.make_tornado 14 | 15 | .. autofunction:: pyledger.handlers.make_wsgi 16 | 17 | -------------------------------------------------------------------------------- /pyledger/pyledger_message.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message PyledgerRequest { 4 | string request = 1; 5 | string contract = 2; 6 | string call = 3; 7 | string client_key = 4; 8 | string session_key = 5; 9 | string user = 6; 10 | string password = 7; 11 | string topic = 8; 12 | bytes data = 9; 13 | } 14 | 15 | message PyledgerResponse { 16 | string response = 1; 17 | string contract = 2; 18 | bool successful = 3; 19 | string topic = 4; 20 | string auth_key = 5; 21 | string session_key = 6; 22 | float timestamp = 7; 23 | bytes data = 8; 24 | } 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pyledger 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /pyledger/server/__init__.py: -------------------------------------------------------------------------------- 1 | from pyledger.server.config import args as cmd_args 2 | from pyledger.server.db import DB 3 | from pyledger.server.contract import register_contract 4 | from pyledger.server.ws import run_server 5 | 6 | 7 | def run(*args, address="ws://127.0.0.1:9000"): 8 | """ 9 | Make Pyledger server with the given contracts as classes 10 | 11 | :param args: Contract classes 12 | :param address: Address to bind the websocket server. Defaults to ws://127.0.0.1:9000 13 | :return: 14 | """ 15 | if cmd_args.sync: 16 | DB.sync_tables() 17 | 18 | for contract in args: 19 | register_contract(contract()) 20 | 21 | run_server(address=address) 22 | -------------------------------------------------------------------------------- /docs/source/distributed.rst: -------------------------------------------------------------------------------- 1 | Distributed ledger with the RAFT consensus protocol 2 | =================================================== 3 | 4 | While having a unique and centralized database allows pyledger to significantly 5 | simplify the ledger infrastructure, it becomes a single point of failure. 6 | However, since the database is a pluggable component in pyledger, you can turn 7 | pyledger into a distributed ledger using a distributed database. 8 | 9 | One interesting choice is `rqlite `_, a 10 | distributed and relational database built on SQLite where all the nodes reach 11 | a consensus based on the RAFT protocol. 12 | 13 | To integrate rqlite with pyledger you must install the packages 14 | sqlalchemy_rqlite and pyrqlite, and run pyledger with the following arguments:: 15 | 16 | python examples/hello/server.py --db rqlite+pyrqlite://localhost:4001 --sync 17 | 18 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=pyledger 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /examples/hello/server.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server import run 18 | from pyledger.server.contract import SimpleContract 19 | 20 | 21 | class Hello(SimpleContract): 22 | def say_hello(self): 23 | return 'Hello' 24 | 25 | run(Hello) 26 | -------------------------------------------------------------------------------- /pyledger/client/__init__.py: -------------------------------------------------------------------------------- 1 | from pyledger.client.ws import WebSocketClientFactory 2 | from pyledger.client.ws import MyClientProtocol 3 | import asyncio 4 | import argparse 5 | 6 | 7 | parser = argparse.ArgumentParser(description='Run the pyledger client') 8 | parser.add_argument('--address', 9 | help="Address to connect to. Defaults to 127.0.0.1", 10 | default='127.0.0.1') 11 | parser.add_argument('--port', 12 | help="Port to connect to. Defaults to 9000", 13 | default='9000') 14 | 15 | 16 | def run(): 17 | args = parser.parse_args() 18 | factory = WebSocketClientFactory('ws://{}:{}'.format(args.address, 19 | args.port)) 20 | factory.protocol = MyClientProtocol 21 | 22 | loop = asyncio.get_event_loop() 23 | coro = loop.create_connection(factory, args.address, int(args.port)) 24 | loop.run_until_complete(coro) 25 | 26 | try: 27 | loop.run_forever() 28 | except KeyboardInterrupt: 29 | loop.shutdown_asyncgens() 30 | loop.close() 31 | print('Bye') 32 | -------------------------------------------------------------------------------- /examples/hello_counter/server.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server import run 18 | from pyledger.server.contract import SimpleContract 19 | 20 | 21 | class Hello(SimpleContract): 22 | counter = 0 23 | 24 | def say_hello(self): 25 | self.counter += 1 26 | return 'Hello {}'.format(self.counter) 27 | 28 | 29 | run(Hello) 30 | -------------------------------------------------------------------------------- /examples/hello_name/server.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server import run 18 | from pyledger.server.contract import SimpleContract 19 | 20 | 21 | class Hello(SimpleContract): 22 | counter = 0 23 | 24 | def say_hello(self, name: str): 25 | self.counter += 1 26 | return 'Hello {} # {}'.format(name, self.counter) 27 | 28 | run(Hello) 29 | -------------------------------------------------------------------------------- /pyledger/verify.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import hashlib 4 | import base64 5 | import json 6 | 7 | 8 | def run(): 9 | parser = argparse.ArgumentParser(description='Pyledger contract ' 10 | 'verification') 11 | parser.add_argument('--data', 12 | help='JSON dump to be verified', 13 | type=str, 14 | required=True) 15 | 16 | args = parser.parse_args() 17 | 18 | with open(args.data) as dump: 19 | statuses = json.load(dump) 20 | 21 | for i, status in enumerate(statuses[1:]): 22 | m = hashlib.sha256() 23 | m.update(base64.b64decode(statuses[i]['hash'])) 24 | m.update(status['when'].encode('utf-8')) 25 | m.update(base64.b64decode(status['attributes'])) 26 | 27 | if m.digest() == base64.b64decode(status['hash']): 28 | sys.stdout.write('.') 29 | else: 30 | sys.stdout.write('\n {}'.format( 31 | 'Inconsistency {}'.format( 32 | status['when'] 33 | ) 34 | )) 35 | 36 | sys.stdout.write('\n') 37 | print('DONE') 38 | -------------------------------------------------------------------------------- /examples/hello_exception/server.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server import run 18 | from pyledger.server.contract import SimpleContract 19 | 20 | 21 | class Hello(SimpleContract): 22 | counter = 0 23 | 24 | def say_hello(self, name: str): 25 | if name == 'Guillen': 26 | raise Exception('You probably mispelled Guillem') 27 | 28 | self.counter += 1 29 | return 'Hello {} # {}'.format(name, self.counter) 30 | 31 | run(Hello) 32 | -------------------------------------------------------------------------------- /pyledger/server/status.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import pickle 3 | 4 | 5 | class BaseStatus(abc.ABC): 6 | """ 7 | Status abstract class 8 | """ 9 | @abc.abstractmethod 10 | def dump(self): 11 | pass 12 | 13 | @abc.abstractmethod 14 | def load(self, dump: bytes): 15 | pass 16 | 17 | @abc.abstractmethod 18 | def to_dict(self): 19 | pass 20 | 21 | 22 | class SimpleStatus(BaseStatus): 23 | """ 24 | Simple status for the smart contract based on a dictionary. 25 | """ 26 | def __init__(self, **kwargs): 27 | self.args_list = [a for a in kwargs] 28 | 29 | for k, v in kwargs.items(): 30 | setattr(self, k, v) 31 | 32 | def dump(self): 33 | return pickle.dumps({k: getattr(self, k) for k in self.args_list}) 34 | 35 | def load(self, dump: bytes): 36 | status = pickle.loads(dump) 37 | self.args_list = [a for a in status] 38 | 39 | for k, v in status.items(): 40 | setattr(self, k, v) 41 | 42 | def to_dict(self): 43 | return {k: getattr(self, k) for k in self.args_list} 44 | 45 | def __contains__(self, item): 46 | return item in self.__dict__ 47 | 48 | def __repr__(self): 49 | return 'Pyledger status with attributes {}'.format( 50 | self.args_list) 51 | 52 | 53 | BaseStatus.register(SimpleStatus) 54 | -------------------------------------------------------------------------------- /.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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Pycharm project dir 92 | .idea -------------------------------------------------------------------------------- /tests/test_4_status.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server.status import BaseStatus, SimpleStatus 18 | 19 | 20 | def test_1(): 21 | status = SimpleStatus(key='value') 22 | assert isinstance(status, BaseStatus) == True 23 | assert ('key' in status) == True 24 | 25 | 26 | def test_2(): 27 | status = SimpleStatus(accounts={}) 28 | status.accounts['My_account'] = 100 29 | assert status.accounts['My_account'] == 100 30 | 31 | 32 | def test_serialization(): 33 | status = SimpleStatus(accounts={}) 34 | status.accounts['My_account'] = 100 35 | 36 | data = status.dump() 37 | del status 38 | 39 | status = SimpleStatus() 40 | status.load(data) 41 | 42 | assert status.accounts['My_account'] == 100 43 | -------------------------------------------------------------------------------- /tests/test_1_sessions.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.pyledger_message_pb2 import PyledgerRequest, PyledgerResponse 18 | from pyledger.server.db import Session 19 | from pyledger.server.handlers import handle_request 20 | 21 | 22 | def test_master_session(): 23 | """ 24 | Get a master session key 25 | """ 26 | request = PyledgerRequest() 27 | request.request = 'session' 28 | request.user = 'master' 29 | request.password = 'password' 30 | 31 | response = PyledgerResponse() 32 | response.ParseFromString(handle_request(request.SerializeToString())) 33 | 34 | assert response.successful == True 35 | 36 | session_key = response.data.decode('utf-8') 37 | session = Session.from_key(session_key) 38 | 39 | assert session.key == session_key 40 | -------------------------------------------------------------------------------- /pyledger/server/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from cryptography.hazmat.primitives import hashes 4 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 5 | 6 | from pyledger.server.config import password_backend, SECRET 7 | from pyledger.server.db import User, DB, Permissions 8 | 9 | permissions_registry = {} 10 | method_permissions_registry = {} 11 | 12 | 13 | def create_master(password): 14 | kpdf = PBKDF2HMAC( 15 | algorithm=hashes.SHA256(), 16 | length=32, 17 | salt=SECRET, 18 | iterations=1000000, 19 | backend=password_backend 20 | ) 21 | master_user = User() 22 | master_user.name = 'master' 23 | master_user.when = datetime.datetime.now() 24 | master_user.set_permissions(Permissions.ROOT) 25 | master_user.set_password(kpdf.derive(password.encode('utf-8'))) 26 | 27 | DB.session.add(master_user) 28 | DB.session.commit() 29 | 30 | 31 | def create_user(name, password): 32 | kpdf = PBKDF2HMAC( 33 | algorithm=hashes.SHA256(), 34 | length=32, 35 | salt=SECRET, 36 | iterations=1000000, 37 | backend=password_backend 38 | ) 39 | user = User() 40 | user.name = name 41 | user.when = datetime.datetime.now() 42 | user.set_permissions(Permissions.USER) 43 | user.set_password(kpdf.derive(password.encode('utf-8'))) 44 | 45 | DB.session.add(user) 46 | DB.session.commit() 47 | 48 | 49 | def allow(permission): 50 | global permissions_registry 51 | 52 | def decorator(func): 53 | permissions_registry[func.__name__] = permission 54 | return func 55 | 56 | return decorator 57 | 58 | 59 | def method_allow(permission): 60 | global method_permissions_registry 61 | 62 | def decorator(func): 63 | if func.__name__ not in method_permissions_registry: 64 | method_permissions_registry[func.__name__] = permission 65 | else: 66 | raise ValueError('A method with the same name registered with different permissions') 67 | return func 68 | 69 | return decorator 70 | 71 | -------------------------------------------------------------------------------- /tests/test_6_call.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import pickle 18 | 19 | from pyledger.pyledger_message_pb2 import PyledgerResponse, PyledgerRequest 20 | from pyledger.server.handlers import handle_request 21 | 22 | 23 | def test_simple_call(): 24 | request = PyledgerRequest() 25 | response = PyledgerResponse() 26 | 27 | request.request = 'call' 28 | request.contract = 'DigitalCurrency' 29 | request.call = 'add_account' 30 | request.data = pickle.dumps({'key': 'new_account'}) 31 | 32 | byte_request = request.SerializeToString() 33 | byte_response = handle_request(byte_request) 34 | response.ParseFromString(byte_response) 35 | 36 | response_data = pickle.loads(response.data) 37 | 38 | assert response.successful == True 39 | assert response_data == response_data 40 | 41 | 42 | def test_exception(): 43 | request = PyledgerRequest() 44 | response = PyledgerResponse() 45 | 46 | request.request = 'call' 47 | request.contract = 'DigitalCurrency' 48 | request.call = 'increment' 49 | request.data = pickle.dumps({'key': 'another_account', 'quantity': 100.0}) 50 | 51 | byte_request = request.SerializeToString() 52 | byte_response = handle_request(byte_request) 53 | response.ParseFromString(byte_response) 54 | 55 | assert response.successful == False 56 | assert response.data == b"Exception in user function: Exception('Account not found',)" 57 | -------------------------------------------------------------------------------- /examples/currency/server.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.server import run 18 | from pyledger.server.contract import SimpleContract 19 | 20 | 21 | class DigitalCurrency(SimpleContract): 22 | accounts = {} 23 | 24 | def add_account(self, key: str): 25 | if key in self.accounts: 26 | raise Exception('Account already exists') 27 | 28 | self.accounts[key] = 0.0 29 | return key 30 | 31 | def increment(self, key: str, quantity: float): 32 | if key not in self.accounts: 33 | raise Exception('Account not found') 34 | 35 | self.accounts[key] += quantity 36 | 37 | def transfer(self, source: str, dest: str, quantity: float): 38 | if source not in self.accounts: 39 | raise Exception('Source account not found') 40 | if dest not in self.accounts: 41 | raise Exception('Destination account not found') 42 | if self.accounts[source] < quantity: 43 | raise Exception('Not enough funds in source account') 44 | if quantity < 0: 45 | raise Exception('You cannot transfer negative currency') 46 | 47 | self.accounts[source] -= quantity 48 | self.accounts[dest] += quantity 49 | 50 | def balance(self, key: str): 51 | if key not in self.accounts: 52 | print(self.accounts) 53 | raise Exception('Account not found') 54 | 55 | return str(self.accounts[key]) 56 | 57 | 58 | run(DigitalCurrency) -------------------------------------------------------------------------------- /tests/test_2_handlers.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.pyledger_message_pb2 import PyledgerResponse, PyledgerRequest 18 | from pyledger.server.handlers import Handler, handler_methods, handle_request 19 | 20 | 21 | def test_handler_methods(): 22 | assert set(handler_methods(Handler())) == { 23 | 'api', 'session', 'echo', 24 | 'contracts', 'verify', 'call', 'status', 25 | 'new_user', 'set_password', 'broadcast' 26 | } 27 | 28 | 29 | def test_failed_message(): 30 | response = PyledgerResponse() 31 | response.ParseFromString(handle_request(b'xxxyyy')) 32 | assert response.successful == False 33 | assert response.data == b'Message not properly formatted' 34 | 35 | 36 | def test_wrong_request(): 37 | request = PyledgerRequest() 38 | response = PyledgerResponse() 39 | request.request = 'blahblah' 40 | response.ParseFromString( 41 | handle_request( 42 | request.SerializeToString() 43 | ) 44 | ) 45 | 46 | assert response.successful == False 47 | assert response.data == b'Request type blahblah not available' 48 | 49 | 50 | def test_call_method(): 51 | request = PyledgerRequest() 52 | response = PyledgerResponse() 53 | 54 | request.request = 'call' 55 | request.contract = 'somecontract' 56 | request.call = 'somemethod' 57 | 58 | byte_request = request.SerializeToString() 59 | byte_response = handle_request(byte_request) 60 | response.ParseFromString(byte_response) 61 | 62 | assert response.successful == False 63 | assert response.data == b'Contract somecontract not available' 64 | -------------------------------------------------------------------------------- /tests/test_8_clientlib.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyledger.client.lib import * 18 | from pyledger.server.handlers import handle_request 19 | 20 | 21 | def test_clientlib_call_request(): 22 | request = call_request(call='add_account', user='master', 23 | password='password', contract='AuthDigitalCurrency', 24 | data={'key': 'another_account'}) 25 | 26 | succesful, response = handle_response(handle_request(request)) 27 | 28 | assert succesful == True 29 | assert response == 'another_account' 30 | 31 | 32 | def test_clientlib_call_request_fail(): 33 | request = call_request(call='add_account', contract='AuthDigitalCurrency', 34 | data={'key': 'yet_another_account'}) 35 | 36 | successful, response = handle_response(handle_request(request)) 37 | 38 | assert successful == False 39 | assert response == 'Not enough permissions' 40 | 41 | 42 | def test_clientlib_call_contracts(): 43 | request = contracts_request() 44 | successful, response = handle_response(handle_request(request)) 45 | 46 | assert successful == True 47 | assert set(response) == {'MyContract', 'DigitalCurrency', 'AuthDigitalCurrency'} 48 | 49 | 50 | def test_clientlib_call_api(): 51 | request = api_request(contract='AuthDigitalCurrency') 52 | successful, response = handle_response(handle_request(request)) 53 | 54 | assert successful == True 55 | assert response == { 56 | 'add_account': {'key': str}, 57 | 'balance': {'key': str}, 58 | 'increment': {'key': str, 'quantity': float}, 59 | 'transfer': {'dest': str, 'quantity': float, 'source': str} 60 | } 61 | -------------------------------------------------------------------------------- /docs/source/users.rst: -------------------------------------------------------------------------------- 1 | Users and permissions 2 | ===================== 3 | 4 | Pyledger supports basic key-based authentication for clients, and the 5 | contracts may be aware if the user was previously created by the administrator 6 | of the ledger. When you run the server for the first time, the ledger server 7 | outputs an admin authentication key, that is stored within the ledger itself:: 8 | 9 | $> python examples/authentication/server.py --sync 10 | Warning: Syncing database at sqlite:// 11 | Warning: Admin key is a1ee413e-0505-49a6-9902-748e87741225 12 | 13 | If you start a client with this key, it will have admin privileges. 14 | 15 | One of the important aspects of admin privileges is the key creation, 16 | which is equivalent of creating a user, since each user is identified by a 17 | random key:: 18 | 19 | $> pyledger-shell --user a1ee413e-0505-49a6-9902-748e87741225 20 | PyLedger simple client 21 | (http://localhost:8888)> key NewUser 22 | Created user Guillem: 79ab6f2d-5fe6-4bf8-9ebd-ee359d9dfa94 23 | (http://localhost:8888)> exit 24 | 25 | This key can be used to authenticate the user, and we can make the contract 26 | aware of the authentication of a client. 27 | 28 | 29 | .. code-block:: python 30 | 31 | def hello(): 32 | def say_hello(attrs): 33 | if attrs.user: 34 | return attrs, 'Hello {}, your key is {}'.format(attrs.user.name, 35 | attrs.user.key) 36 | else: 37 | raise Exception('Not authenticated') 38 | 39 | contract = Builder('Hello') 40 | contract.add_method(say_hello) 41 | 42 | return contract 43 | 44 | The attrs object contains a copy of the data stored about the user, like its 45 | name or the user key. If the user was not authenticated, ``attrs.user`` is 46 | set as ``None``. 47 | 48 | We can now start the client with the new user key:: 49 | 50 | $> pyledger-shell --user 79ab6f2d-5fe6-4bf8-9ebd-ee359d9dfa94 51 | (http://localhost:8888)> api Hello 52 | say_hello ( ) 53 | 54 | (http://localhost:8888)> call Hello say_hello 55 | Hello Guillem, your key is 79ab6f2d-5fe6-4bf8-9ebd-ee359d9dfa94 56 | (http://localhost:8888)> exit 57 | 58 | 59 | .. important:: 60 | 61 | There is only one user called ``admin``, that is assigned the key that is 62 | printed when the ledger is started for the first time with the ``--sync`` 63 | option. This means that, ``if attrs.user.name == 'admin'`` checks if the 64 | current user is in fact the owner of the ledger. 65 | 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Pyledger. A simple ledger for smart contracts implemented in Python 4 | # Copyright (C) 2017 Guillem Borrell Nogueras 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU Affero General Public License as published 8 | # by the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | 19 | 20 | from setuptools import setup 21 | 22 | __version__ = None 23 | with open('pyledger/__init__.py') as f: 24 | exec(f.read()) 25 | 26 | long_description = """ 27 | .. image:: https://badge.fury.io/py/pyledger.svg 28 | :target: https://badge.fury.io/py/pyledger 29 | 30 | .. image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat 31 | :target: https://pyledger.readthedocs.io/en/latest 32 | 33 | .. image:: https://badge.fury.io/gh/guillemborrell%2Fpyledger.svg 34 | :target: https://badge.fury.io/gh/guillemborrell%2Fpyledger 35 | """ 36 | 37 | setup(name='pyledger', 38 | version=__version__, 39 | description='A simple ledger for smart contracts written in Python', 40 | long_description=long_description, 41 | author='Guillem Borrell', 42 | author_email='guillemborrell@gmail.com', 43 | packages=['pyledger', 'pyledger'], 44 | classifiers=[ 45 | 'Development Status :: 3 - Alpha', 46 | 'Environment :: Console', 47 | 'Intended Audience :: Developers', 48 | 'Programming Language :: Python', 49 | 'Programming Language :: Python :: 3 :: Only', 50 | 'Programming Language :: Python :: 3.5', 51 | 'Programming Language :: Python :: 3.6', 52 | 'License :: OSI Approved :: GNU Affero General Public License v3' 53 | ], 54 | setup_requires=['pytest-runner', 'pytest'], 55 | install_requires=['protobuf>=3.0.0', 'sqlalchemy', 56 | 'autobahn', 'google', 'cryptography'], 57 | entry_points={ 58 | 'console_scripts': ['pyledger-shell=pyledger.client:run', 59 | 'pyledger-verify=pyledger.verify:run'] 60 | } 61 | ) 62 | -------------------------------------------------------------------------------- /tests/test_5_handlers.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import pickle 18 | 19 | from pyledger.pyledger_message_pb2 import PyledgerResponse, PyledgerRequest 20 | from pyledger.server.handlers import handle_request 21 | 22 | 23 | # Test handlers again now that there are some contracts stored 24 | 25 | def test_contracts(): 26 | request = PyledgerRequest() 27 | response = PyledgerResponse() 28 | 29 | request.request = 'contracts' 30 | 31 | byte_request = request.SerializeToString() 32 | byte_response = handle_request(byte_request) 33 | response.ParseFromString(byte_response) 34 | 35 | assert response.successful == True 36 | 37 | contracts = pickle.loads(response.data) 38 | 39 | assert set(contracts) == {'MyContract', 'DigitalCurrency'} 40 | 41 | 42 | def test_api(): 43 | request = PyledgerRequest() 44 | response = PyledgerResponse() 45 | 46 | request.request = 'api' 47 | request.contract = 'DigitalCurrency' 48 | 49 | byte_request = request.SerializeToString() 50 | byte_response = handle_request(byte_request) 51 | response.ParseFromString(byte_response) 52 | api = pickle.loads(response.data) 53 | 54 | assert response.successful == True 55 | assert api == { 56 | 'add_account': {'key': str}, 57 | 'balance': {'key': str}, 58 | 'increment': {'key': str, 'quantity': float}, 59 | 'transfer': {'dest': str, 'quantity': float, 'source': str} 60 | } 61 | 62 | 63 | def test_status(): 64 | request = PyledgerRequest() 65 | response = PyledgerResponse() 66 | 67 | request.request = 'status' 68 | request.contract = 'DigitalCurrency' 69 | 70 | byte_request = request.SerializeToString() 71 | byte_response = handle_request(byte_request) 72 | response.ParseFromString(byte_response) 73 | status = pickle.loads(response.data) 74 | 75 | assert response.successful == True 76 | assert status == {'accounts': {}} 77 | -------------------------------------------------------------------------------- /pyledger/server/config.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import sys 18 | import argparse 19 | from collections import namedtuple 20 | from cryptography.hazmat.backends import default_backend 21 | 22 | # USER CONFIGURATION AT THE END OF THE FILE # 23 | 24 | if len(sys.argv) > 2 and 'test' in sys.argv[2]: 25 | argstype = namedtuple('Arguments', ['db', 'debug', 'sync', 'port', 'test']) 26 | args = argstype(db='sqlite://', debug=False, sync=True, port=8888, 27 | test=True) 28 | 29 | elif 'pytest' in sys.argv[0]: 30 | argstype = namedtuple('Arguments', ['db', 'debug', 'sync', 'port', 'test']) 31 | args = argstype(db='sqlite://', debug=False, sync=True, port=8888, 32 | test=True) 33 | 34 | elif 'sphinx' in sys.argv[0]: 35 | argstype = namedtuple('Arguments', ['db', 'debug', 'sync', 'port', 'test']) 36 | args = argstype(db='sqlite://', debug=False, sync=False, port=8888, 37 | test=False) 38 | 39 | elif 'pydevconsole' in sys.argv[0]: 40 | argstype = namedtuple('Arguments', ['db', 'debug', 'sync', 'port', 'test']) 41 | args = argstype(db='sqlite://', debug=False, sync=False, port=8888, 42 | test=False) 43 | 44 | elif 'pyledger-verify' in sys.argv[0]: 45 | pass 46 | 47 | elif 'pyledger-shell' in sys.argv[0]: 48 | pass 49 | 50 | else: 51 | parser = argparse.ArgumentParser(description='Run the PyLedger server') 52 | parser.add_argument('--db', help="SQLAlchemy database address", 53 | type=str, default="sqlite://") 54 | parser.add_argument('--sync', action='store_true') 55 | parser.add_argument('--debug', action='store_true', default=False) 56 | parser.add_argument('--port', type=int, default=8888) 57 | parser.add_argument('--test', action='store_true', default=False) 58 | args = parser.parse_args() 59 | 60 | 61 | password_backend = default_backend() 62 | 63 | # User configurable section 64 | 65 | # - Put a very secret word here 66 | SECRET = b'test' 67 | 68 | # - Lifetime of the sessions in hours 69 | LIFETIME = 1 70 | -------------------------------------------------------------------------------- /tests/test_7_contract_auth.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from pyledger.pyledger_message_pb2 import PyledgerResponse, PyledgerRequest 4 | from pyledger.server.auth import method_allow, Permissions 5 | from pyledger.server.contract import SimpleContract, register_contract 6 | from pyledger.server.handlers import handle_request 7 | 8 | 9 | def test_0_register_auth_contract(): 10 | class AuthDigitalCurrency(SimpleContract): 11 | accounts = {} 12 | 13 | @method_allow(Permissions.ROOT) 14 | def add_account(self, key: str): 15 | if key in self.accounts: 16 | raise Exception('Account already exists') 17 | 18 | self.accounts[key] = 0.0 19 | return key 20 | 21 | @method_allow(Permissions.ROOT) 22 | def increment(self, key: str, quantity: float): 23 | if key not in self.accounts: 24 | raise Exception('Account not found') 25 | 26 | self.accounts[key] += quantity 27 | 28 | @method_allow(Permissions.USER) 29 | def transfer(self, source: str, dest: str, quantity: float): 30 | if source not in self.accounts: 31 | raise Exception('Source account not found') 32 | if dest not in self.accounts: 33 | raise Exception('Destination account not found') 34 | if self.accounts[source] < quantity: 35 | raise Exception('Not enough funds in source account') 36 | if quantity < 0: 37 | raise Exception('You cannot transfer negative currency') 38 | 39 | self.accounts[source] -= quantity 40 | self.accounts[dest] += quantity 41 | 42 | @method_allow(Permissions.USER) 43 | def balance(self, key: str): 44 | if key not in self.accounts: 45 | print(self.accounts) 46 | raise Exception('Account not found') 47 | 48 | return str(self.accounts[key]) 49 | 50 | register_contract(AuthDigitalCurrency()) 51 | 52 | request = PyledgerRequest() 53 | response = PyledgerResponse() 54 | 55 | request.request = 'call' 56 | request.contract = 'AuthDigitalCurrency' 57 | request.call = 'add_account' 58 | request.data = pickle.dumps({'key': 'new_account'}) 59 | 60 | byte_request = request.SerializeToString() 61 | byte_response = handle_request(byte_request) 62 | response.ParseFromString(byte_response) 63 | 64 | assert response.successful == False 65 | assert response.data == b'Not enough permissions' 66 | 67 | 68 | def test_access_contract_as_root(): 69 | request = PyledgerRequest() 70 | response = PyledgerResponse() 71 | 72 | request.request = 'call' 73 | request.contract = 'AuthDigitalCurrency' 74 | request.call = 'add_account' 75 | request.user = 'master' 76 | request.password = 'password' 77 | request.data = pickle.dumps({'key': 'new_account'}) 78 | 79 | byte_request = request.SerializeToString() 80 | byte_response = handle_request(byte_request) 81 | response.ParseFromString(byte_response) 82 | 83 | assert response.successful == True 84 | 85 | response_data = pickle.loads(response.data) 86 | assert response_data == 'new_account' 87 | -------------------------------------------------------------------------------- /tests/test_0_users.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import datetime 18 | import pickle 19 | 20 | from pyledger.pyledger_message_pb2 import PyledgerRequest, PyledgerResponse 21 | from pyledger.server.auth import create_master, create_user 22 | from pyledger.server.config import LIFETIME 23 | from pyledger.server.db import User, DB, Permissions, Session 24 | from pyledger.server.handlers import handle_request 25 | 26 | DB.sync_tables() 27 | 28 | 29 | def test_0_master_user(): 30 | """ 31 | Create a master user 32 | """ 33 | create_master('password') 34 | user = User.from_name('master') 35 | assert user.get_permissions() == Permissions.ROOT 36 | assert user.check_password('password') == True 37 | 38 | # Create dummy session 39 | session = Session() 40 | session.user = user 41 | session.key = 'test_session' 42 | session.registered = datetime.datetime.now() 43 | session.until = datetime.datetime.now() + datetime.timedelta(hours=LIFETIME) 44 | 45 | DB.session.add(session) 46 | DB.session.commit() 47 | 48 | 49 | def test_1_user(): 50 | """ 51 | Create a normal user 52 | """ 53 | create_user('user', 'password') 54 | user = User.from_name('user') 55 | assert user.get_permissions() == Permissions.USER 56 | assert user.check_password('password') == True 57 | 58 | 59 | def test_2_create_user(): 60 | """ 61 | Create a user from the API 62 | """ 63 | request = PyledgerRequest() 64 | request.request = 'new_user' 65 | request.user = 'master' 66 | request.password = 'password' 67 | request.session_key = 'test_session' 68 | request.data = pickle.dumps(('user2', 'new_password')) 69 | 70 | response = PyledgerResponse() 71 | response.ParseFromString(handle_request(request.SerializeToString())) 72 | assert response.successful == True 73 | assert response.data == b'user2' 74 | 75 | user = User.from_name('user2') 76 | assert user.get_permissions() == Permissions.USER 77 | assert user.check_password('new_password') 78 | 79 | 80 | def test_3_create_without_permissions(): 81 | """ 82 | Try to create a user without the permissions 83 | """ 84 | request = PyledgerRequest() 85 | request.request = 'new_user' 86 | request.user = 'user' 87 | request.password = 'password' 88 | request.data = pickle.dumps(('user3', 'new_password')) 89 | 90 | response = PyledgerResponse() 91 | response.ParseFromString(handle_request(request.SerializeToString())) 92 | assert response.successful == False 93 | assert response.data == b'Not enough permissions' 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /pyledger/client/lib.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import pickle 18 | 19 | from pyledger.pyledger_message_pb2 import PyledgerResponse, PyledgerRequest 20 | 21 | 22 | def auth_info(kwargs): 23 | user = '' 24 | password = '' 25 | 26 | if 'user' in kwargs: 27 | user = kwargs['user'] 28 | 29 | if 'password' in kwargs: 30 | password = kwargs['password'] 31 | 32 | return user, password 33 | 34 | 35 | def session_info(kwargs): 36 | session = '' 37 | 38 | if 'session' in kwargs: 39 | session = kwargs['session'] 40 | 41 | return session 42 | 43 | 44 | def call_request(**kwargs): 45 | request = PyledgerRequest() 46 | 47 | request.user, request.password = auth_info(kwargs) 48 | request.session_key = session_info(kwargs) 49 | request.request = 'call' 50 | 51 | if 'contract' not in kwargs: 52 | raise ValueError('Contract should be a keyword argument') 53 | request.contract = kwargs['contract'] 54 | 55 | if 'call' not in kwargs: 56 | raise ValueError('Call should be a keyword argument') 57 | request.call = kwargs['call'] 58 | 59 | if 'data' not in kwargs: 60 | raise ValueError('Data should be a keyword argument') 61 | request.data = pickle.dumps(kwargs['data']) 62 | 63 | return request.SerializeToString() 64 | 65 | 66 | def contracts_request(**kwargs): 67 | # This is simple, doesn't require authentication. 68 | request = PyledgerRequest() 69 | request.request = 'contracts' 70 | return request.SerializeToString() 71 | 72 | 73 | def api_request(**kwargs): 74 | request = PyledgerRequest() 75 | request.request = 'api' 76 | 77 | if 'contract' not in kwargs: 78 | raise ValueError('Contract should be a keyword argument') 79 | 80 | if kwargs['contract']: 81 | request.contract = kwargs['contract'] 82 | else: 83 | raise ValueError('You should give a contract name') 84 | 85 | return request.SerializeToString() 86 | 87 | 88 | def broadcast_request(message): 89 | request = PyledgerRequest() 90 | request.request = 'broadcast' 91 | request.data = pickle.dumps(message) 92 | 93 | return request.SerializeToString() 94 | 95 | 96 | def handle_response(bin_response, callback=None): 97 | response = PyledgerResponse() 98 | response.ParseFromString(bin_response) 99 | 100 | if response.successful: 101 | if callback: 102 | response_data = pickle.loads(response.data) 103 | print('Executing callback...') 104 | callback(response_data) 105 | return True, response_data 106 | else: 107 | return True, pickle.loads(response.data) 108 | 109 | else: 110 | return False, response.data.decode('utf-8') 111 | -------------------------------------------------------------------------------- /pyledger/server/contract.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import abc 18 | import datetime 19 | import inspect 20 | 21 | from pyledger.server.db import DB, Contract, Status 22 | from pyledger.server.status import SimpleStatus 23 | 24 | contract_registry = {} 25 | 26 | 27 | class BaseContract(abc.ABC): 28 | pass 29 | 30 | 31 | class SimpleContract(BaseContract): 32 | """ 33 | Contract that uses SimpleStatus for serialization. 34 | 35 | The goal of this class is to make a contact feel just like a Python class. 36 | """ 37 | _status_class = SimpleStatus 38 | 39 | BaseContract.register(SimpleContract) 40 | 41 | 42 | def methods(contract): 43 | """ 44 | Obtain methods from the contract 45 | 46 | :param contract: 47 | :return: 48 | """ 49 | methods = {} 50 | 51 | for name, function in inspect.getmembers(contract, 52 | predicate=inspect.ismethod): 53 | if not name == '__init__': 54 | methods[name] = function 55 | 56 | return methods 57 | 58 | 59 | def api(contract): 60 | api_spec = {} 61 | contract_methods = methods(contract) 62 | for method in contract_methods: 63 | function_spec = {} 64 | sig = inspect.signature(contract_methods[method]) 65 | for param in sig.parameters: 66 | function_spec[param] = sig.parameters[param].annotation 67 | 68 | api_spec[method] = function_spec 69 | 70 | return api_spec 71 | 72 | 73 | def signatures(contract): 74 | contract_signatures = {} 75 | contract_methods = methods(contract) 76 | for k, method in contract_methods.items(): 77 | contract_signatures[k] = inspect.signature(method) 78 | 79 | return contract_signatures 80 | 81 | 82 | def status(contract): 83 | all_attributes = inspect.getmembers( 84 | contract, 85 | predicate=lambda a: not(inspect.isroutine(a))) 86 | 87 | attributes = {} 88 | 89 | for attribute in all_attributes: 90 | if not attribute[0].startswith('_'): 91 | attributes[attribute[0]] = attribute[1] 92 | 93 | return contract._status_class(**attributes) 94 | 95 | 96 | def register_contract(contract, description=''): 97 | """ 98 | Register a contract and make it 99 | :param contract: 100 | :param description: 101 | :return: 102 | """ 103 | global contract_registry 104 | 105 | if contract.__class__.__name__ in contract_registry: 106 | raise ValueError('A contract with the same name already registered') 107 | else: 108 | contract_registry[contract.__class__.__name__] = contract 109 | 110 | db_contract = Contract() 111 | db_contract.name = contract.__class__.__name__ 112 | db_contract.created = datetime.datetime.now() 113 | db_contract.description = description 114 | 115 | first_status = Status() 116 | first_status.contract = db_contract 117 | first_status.when = datetime.datetime.now() 118 | first_status.attributes = status(contract).dump() 119 | # Genesis key is the name of the contract 120 | first_status.key = contract.__class__.__name__.encode('utf-8') 121 | 122 | DB.session.add(db_contract) 123 | DB.session.add(first_status) 124 | DB.session.commit() 125 | -------------------------------------------------------------------------------- /pyledger/client/ws.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 18 | WebSocketClientFactory 19 | from asyncio.streams import StreamWriter, FlowControlMixin 20 | from pyledger.client.repl import parse 21 | from pyledger.client.lib import handle_response 22 | from uuid import uuid4 23 | import os 24 | import sys 25 | import asyncio 26 | from pprint import pprint 27 | 28 | 29 | reader, writer = None, None 30 | 31 | 32 | async def stdio(loop=None): 33 | if loop is None: 34 | loop = asyncio.get_event_loop() 35 | 36 | reader = asyncio.StreamReader() 37 | reader_protocol = asyncio.StreamReaderProtocol(reader) 38 | 39 | writer_transport, writer_protocol = await loop.connect_write_pipe( 40 | FlowControlMixin, os.fdopen(0, 'wb')) 41 | writer = StreamWriter(writer_transport, writer_protocol, None, loop) 42 | 43 | await loop.connect_read_pipe(lambda: reader_protocol, sys.stdin) 44 | 45 | return reader, writer 46 | 47 | 48 | async def async_input(message, protocol): 49 | if isinstance(message, str): 50 | message = message.encode('utf8') 51 | 52 | global reader, writer 53 | if (reader, writer) == (None, None): 54 | reader, writer = await stdio() 55 | 56 | writer.write(message) 57 | await writer.drain() 58 | 59 | line = await reader.readline() 60 | # This is where everything happens in the client side 61 | return parse(line, protocol=protocol) 62 | 63 | 64 | class MyClientProtocol(WebSocketClientProtocol): 65 | topics = [] 66 | 67 | def onConnect(self, response): 68 | print("Connected to server: {0}".format(response.peer)) 69 | 70 | async def onOpen(self): 71 | print("Pyledger REPL client, write 'help' for help or 'help command' " 72 | "for help on a specific command") 73 | 74 | while True: 75 | success, message = await async_input('PL >>> ', self) 76 | if success: 77 | # Create topic for subscription. 78 | if message.startswith(36*b'0'): 79 | topic = message[:36] 80 | message = message[36:] 81 | else: 82 | topic = str(uuid4()).encode() 83 | self.topics.append(topic) 84 | self.sendMessage(topic + message, isBinary=True) 85 | else: 86 | print(message) 87 | 88 | if message == 'Successfully closed, you can kill this with Ctrl-C': 89 | break 90 | 91 | await asyncio.sleep(0.1) 92 | 93 | def onMessage(self, payload, isBinary): 94 | topic = payload[:36] 95 | payload = payload[36:] 96 | 97 | # 36 zero-bytes means broadcast 98 | if topic in self.topics or topic == 36*b'0': 99 | if topic != 36*b'0': 100 | self.topics.remove(topic) 101 | success, response = handle_response(payload) 102 | pprint(response) 103 | else: 104 | pprint(topic) 105 | 106 | def onClose(self, wasClean, code, reason): 107 | print("WebSocket connection closed: {}; {}".format(code, reason)) 108 | 109 | 110 | if __name__ == '__main__': 111 | factory = WebSocketClientFactory('ws://127.0.0.1:9000') 112 | factory.protocol = MyClientProtocol 113 | 114 | loop = asyncio.get_event_loop() 115 | coro = loop.create_connection(factory, '127.0.0.1', 9000) 116 | loop.run_until_complete(coro) 117 | try: 118 | loop.run_forever() 119 | except KeyboardInterrupt: 120 | loop.shutdown_asyncgens() 121 | loop.close() 122 | print('Bye') 123 | -------------------------------------------------------------------------------- /pyledger/client/repl.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from functools import partial 18 | from pyledger.client.lib import * 19 | 20 | 21 | def disconnect(*args, protocol=None): 22 | protocol.sendClose(code=1000, reason='Client manual disconnection') 23 | return False, 'Successfully closed, you can kill this with Ctrl-C' 24 | 25 | 26 | def contracts(*args, protocol=None): 27 | return True, contracts_request() 28 | 29 | 30 | def api(*args, protocol=None): 31 | if not args: 32 | return False, 'No contract' 33 | 34 | contract, *_ = args 35 | try: 36 | request = api_request(contract=contract) 37 | return True, request 38 | except ValueError as e: 39 | return False, str(e) 40 | 41 | 42 | def broadcast(*args, protocol=None): 43 | if args: 44 | return True, 36*b'0' + broadcast_request(args[0].encode()) 45 | else: 46 | return False, 'No message to bcast provided' 47 | 48 | 49 | def call(*args, protocol=None): 50 | if not args: 51 | return False, 'No arguments' 52 | 53 | elif len(args) == 1: 54 | return False, 'Wrong number of arguments' 55 | 56 | elif len(args) == 2: 57 | contract, method = args 58 | args = {} 59 | # Check if method can have no argument 60 | 61 | else: 62 | contract, method, *data = args 63 | if len(data) % 2 != 0: 64 | return False, 'Call with pairs of key value arguments' 65 | 66 | args = {} 67 | for key, arg in zip(data[::2], data[1::2]): 68 | args[key] = arg 69 | 70 | try: 71 | request = call_request(contract=contract, call=method, data=args) 72 | return True, request 73 | except ValueError as e: 74 | return False, str(e) 75 | 76 | 77 | instructions = { 78 | 'disconnect': disconnect, 79 | 'contracts': contracts, 80 | 'api': api, 81 | 'call': call, 82 | 'broadcast': broadcast 83 | } 84 | 85 | command_help = { 86 | 'disconnect': """ 87 | This is the help of disconnect 88 | """, 89 | 'contracts': """ 90 | This is the help of contracts 91 | """, 92 | 'api': """ 93 | This is the help of api 94 | """, 95 | 'call': """ 96 | This is the help of call 97 | """, 98 | 'broadcast': """ 99 | this is the help of broadcast 100 | """ 101 | } 102 | 103 | help_str = """ 104 | The Pyledger REPL is a console to interact with a Pyledger server. 105 | The list of available commands is the following 106 | 107 | help Shows this help 108 | disconnect Disconnects from the server in a clean way. 109 | contracts Lists the available contracts in the server 110 | api Shows the api for a particular contract 111 | call Calls a method of a contract 112 | broadcast Broadcast message all clients 113 | 114 | This client may have some limitations respect to a custom client. 115 | For instance, the server may push notifications to the clients, 116 | and using the client API, you could define callbacks to those 117 | pushed messages. 118 | 119 | Read the full documentation in http://pyledger.readthedocs.io 120 | """ 121 | 122 | 123 | def general_parser(line, protocol=None, instruction_dict=None, 124 | user_instruction_dict=None): 125 | message = line.decode('utf-8').replace('\r', '').replace('\n', '') 126 | message_words = message.split() 127 | 128 | if not message: 129 | return False, '' 130 | 131 | if 'help' in message_words[0].casefold(): 132 | if len(message_words) == 1: 133 | return False, help_str 134 | else: 135 | if message_words[1].casefold() in command_help: 136 | return False, command_help[message_words[1].casefold()] 137 | 138 | if message_words[0] not in instruction_dict: 139 | return False, 'Command could not be parsed' 140 | else: 141 | successful, message = instruction_dict[message_words[0]]( 142 | *message_words[1:], protocol=protocol) 143 | 144 | return successful, message 145 | 146 | parse = partial(general_parser, instruction_dict=instructions) 147 | -------------------------------------------------------------------------------- /docs/source/chain.rst: -------------------------------------------------------------------------------- 1 | The status chain 2 | ================ 3 | 4 | Pyledger does not use a blockchain or any similar protocol because it would 5 | be very inefficient for a tool that is not distributed. The internal storage 6 | for every contract is not divided in blocks, since each state is stored as a 7 | register in a SQL database. 8 | 9 | One of the important features of the blockchain is that it is impossible for 10 | anyone, even the owner of the data, to tamper with its contents. Pyledger 11 | also has this feature, but in a slightly different fashion. All the statuses 12 | stored in the ledger for every contract are hashed with the the previous 13 | state's hash and the date and time of insertion. It is 14 | therefore impossible to modify a register of the database without leaving 15 | an obvious footprint on the sequence of hashes. This is a kind 16 | of *status chain* instead of a block chain. 17 | 18 | All the hashes are secure, since pyledger uses SHA-256, the same as in 19 | Bitcoin. This means that one can verify that anyone hasn't been tampering 20 | with the backend database that stores the statuses. We can use the *verify* 21 | command of the client to check that the greeter smart contract works as 22 | expected. We will start an example session to understand some of the features 23 | of this *status chain* with one of the previous examples:: 24 | 25 | (env) $> pyledger-shell 26 | PyLedger simple client 27 | (http://localhost:8888)> call Hello say_hello Guillem 28 | Hello Guillem for time #1 29 | (http://localhost:8888)> call Hello say_hello Guillem 30 | Hello Guillem for time #2 31 | (http://localhost:8888)> call Hello say_hello Guillem 32 | Hello Guillem for time #3 33 | (http://localhost:8888)> status Hello 34 | {'counter': 3} 35 | (http://localhost:8888)> verify Hello 36 | 'Contract OK' 37 | 38 | The *status* command checks and prints the last status of the contract 39 | attributes, while the *verify* command verifies **at the server side** that 40 | all the statuses of the attributes are consistent. If any of the statuses 41 | is inconsistent with the chain, that inconsistency and its timestamp will be 42 | printed. 43 | 44 | Of course, you may not trust the server-side operations on the *status 45 | chain*, which is quite smart. For that reason you can dump all the statuses 46 | of the contract with their corresponding signatures and timestamps with the 47 | following command:: 48 | 49 | (http://localhost:8888)> status Hello dump ./hello-ledger.json 50 | Contract data dump at ./hello-ledger.json 51 | (http://localhost:8888)> exit 52 | 53 | The dumped file looks like this:: 54 | 55 | [{'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsAcy4=', 56 | 'hash': 'Z2VuZXNpcw==', 57 | 'when': '2017-03-15T18:24:27.523828'}, 58 | {'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsBcy4=', 59 | 'hash': 'eRs+YxhvKIyUdl++TQZ5sCcMDE0aoaNKn1swFQ44bMM=', 60 | 'when': '2017-03-15T18:24:38.846864'}, 61 | {'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsCcy4=', 62 | 'hash': 'ZGELWR6y7n+hneBbR+8x9PwaRpBi3Bi0CI/T+9J7ccY=', 63 | 'when': '2017-03-15T18:24:39.580593'}, 64 | {'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsDcy4=', 65 | 'hash': '7B+OH/4xxJz6J6NOixl32F1vXrWZFNQKMR7pe/HO7gY=', 66 | 'when': '2017-03-15T18:24:39.925244'}] 67 | 68 | Every state has three properties. The first one is the hash, which is a 69 | base64-encoded SHA-526 hash; the second one is the timestamp of the addition to 70 | the database in the ISO 8601 format, while the third are all the attributes 71 | of the contract, also properly serialized. 72 | 73 | .. note:: 74 | 75 | If you want to deserialize the attributes to look inside that funny 76 | string, they are pickled and then base64 encoded 77 | 78 | If you want to verify the dumped statuses of the contract you can use the 79 | utility *pyledger-verify*:: 80 | 81 | $> pyledger-verify --data hello-ledger.json 82 | ... 83 | DONE 84 | 85 | where every dot is one successfully verified step. 86 | 87 | If you tamper with this file, or the database that stores the information, 88 | even changing a single bit, the status chain will inform you of the 89 | inconsistency giving its timestamp. 90 | 91 | .. code-block:: python 92 | :emphasize-lines: 4 93 | 94 | [{'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsAcy4=', 95 | 'hash': 'Z2VuZXNpcw==', 96 | 'when': '2017-03-15T18:24:27.523828'}, 97 | {'attributes': 'gAN9cQBYBwAABGNvdW50ZXJxAUsBcy4=', 98 | 'hash': 'eRs+YxhvKIyUdl++TQZ5sCcMDE0aoaNKn1swFQ44bMM=', 99 | 'when': '2017-03-15T18:24:38.846864'}, 100 | {'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsCcy4=', 101 | 'hash': 'ZGELWR6y7n+hneBbR+8x9PwaRpBi3Bi0CI/T+9J7ccY=', 102 | 'when': '2017-03-15T18:24:39.580593'}, 103 | {'attributes': 'gAN9cQBYBwAAAGNvdW50ZXJxAUsDcy4=', 104 | 'hash': '7B+OH/4xxJz6J6NOixl32F1vXrWZFNQKMR7pe/HO7gY=', 105 | 'when': '2017-03-15T18:24:39.925244'}] 106 | 107 | 108 | This is the output of the pyledger-verify tool with the manipulated file:: 109 | 110 | $> pyledger-verify --data hello-ledger.json 111 | Inconsistency 2017-03-15T18:24:38.846864.. 112 | DONE 113 | 114 | -------------------------------------------------------------------------------- /pyledger/server/ws.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from autobahn.asyncio.websocket import WebSocketServerProtocol, \ 18 | WebSocketServerFactory 19 | from pyledger.server.handlers import handle_request 20 | from pyledger.server.contract import register_contract, SimpleContract 21 | from pyledger.server.db import DB 22 | from pyledger.server.auth import Permissions, method_allow 23 | 24 | import asyncio 25 | 26 | loop = asyncio.get_event_loop() 27 | 28 | 29 | class Protocol(WebSocketServerProtocol): 30 | contract = None 31 | bcast_topic = 36*b'0' 32 | 33 | def onConnect(self, request): 34 | self.factory.register(self) 35 | print("Client connecting: {0}".format(request.peer)) 36 | 37 | def onOpen(self): 38 | print("WebSocket connection open.") 39 | 40 | def onMessage(self, payload, isBinary): 41 | try: 42 | topic = payload[:36] 43 | payload = payload[36:] 44 | response = handle_request(payload) 45 | except: 46 | response = b'ERROR' 47 | 48 | if topic == 36*b'0': 49 | self.factory.broadcast(topic + response) 50 | else: 51 | self.sendMessage(topic + response, True) 52 | 53 | def onClose(self, wasClean, code, reason): 54 | print("WebSocket connection closed: {0}".format(reason)) 55 | 56 | def _connectionLost(self, reason): 57 | WebSocketServerProtocol._connectionLost(self, reason) 58 | self.factory.unregister(self) 59 | 60 | 61 | class BroadcastServerFactory(WebSocketServerFactory): 62 | """ 63 | Simple broadcast server broadcasting any message it receives to all 64 | currently connected clients. 65 | """ 66 | def __init__(self, url): 67 | WebSocketServerFactory.__init__(self, url) 68 | self.clients = [] 69 | 70 | def register(self, client): 71 | if client not in self.clients: 72 | print("registered client {}".format(client.peer)) 73 | self.clients.append(client) 74 | 75 | def unregister(self, client): 76 | if client in self.clients: 77 | print("unregistered client {}".format(client.peer)) 78 | self.clients.remove(client) 79 | 80 | def broadcast(self, msg): 81 | print("broadcasting message '{}' ..".format(msg)) 82 | for c in self.clients: 83 | c.sendMessage(msg, True) 84 | print("message sent to {}".format(c.peer)) 85 | 86 | 87 | def run_server(address="ws://127.0.0.1:9000"): 88 | factory = BroadcastServerFactory(address) 89 | factory.protocol = Protocol 90 | server = loop.create_server(factory, 91 | '0.0.0.0', 92 | int(address.split(':')[2]) 93 | ) 94 | task = loop.run_until_complete(server) 95 | 96 | try: 97 | print('Starting event loop...') 98 | loop.run_forever() 99 | except KeyboardInterrupt: 100 | pass 101 | finally: 102 | task.close() 103 | loop.close() 104 | 105 | 106 | if __name__ == '__main__': 107 | DB.sync_tables() 108 | 109 | # Base contract for testing 110 | 111 | class DigitalCurrency(SimpleContract): 112 | accounts = {} 113 | 114 | def add_account(self, key: str): 115 | if key in self.accounts: 116 | raise Exception('Account already exists') 117 | 118 | self.accounts[key] = 0.0 119 | return key 120 | 121 | def increment(self, key: str, quantity: float): 122 | if key not in self.accounts: 123 | raise Exception('Account not found') 124 | 125 | self.accounts[key] += quantity 126 | 127 | def transfer(self, source: str, dest: str, quantity: float): 128 | if source not in self.accounts: 129 | raise Exception('Source account not found') 130 | if dest not in self.accounts: 131 | raise Exception('Destination account not found') 132 | if self.accounts[source] < quantity: 133 | raise Exception('Not enough funds in source account') 134 | if quantity < 0: 135 | raise Exception('You cannot transfer negative currency') 136 | 137 | self.accounts[source] -= quantity 138 | self.accounts[dest] += quantity 139 | 140 | def balance(self, key: str): 141 | if key not in self.accounts: 142 | print(self.accounts) 143 | raise Exception('Account not found') 144 | 145 | return str(self.accounts[key]) 146 | 147 | register_contract(DigitalCurrency()) 148 | print('Contract registered successfully') 149 | 150 | run_server() 151 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pyledger 2 | ======== 3 | 4 | **A simple ledger for smart contracts written in Python** 5 | 6 | .. image:: https://badge.fury.io/py/pyledger.svg 7 | :target: https://badge.fury.io/py/pyledger 8 | 9 | .. image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat 10 | :target: https://pyledger.readthedocs.io/en/latest 11 | 12 | .. image:: https://badge.fury.io/gh/guillemborrell%2Fpyledger.svg 13 | :target: https://badge.fury.io/gh/guillemborrell%2Fpyledger 14 | 15 | Smart contracts are taking over the financial ecosystem, but most platforms 16 | are terribly complicated given their parallel nature. What happens is that, 17 | if you don't need to deal with parallelism, building a ledger for smart 18 | contracts is relatively easy. Here's where Pyledger comes into play. 19 | 20 | Assume that you want to create a smart contract to implement a digital 21 | currency system. You have some features you consider necessary, namely 22 | creating accounts, adding currency to any account, checking the balance and 23 | transfer some amount. 24 | 25 | A smart contract is an application, so you need to code to create one. In 26 | Pyledger you can implement your smart contract in Python. In a few words, a 27 | smart contract in Pyledger is a Python class 28 | 29 | .. code-block:: python 30 | 31 | from pyledger.server.contract import SimpleContract 32 | 33 | class DigitalCurrency(SimpleContract): 34 | accounts = {} 35 | 36 | def add_account(self, key: str): 37 | if key in self.accounts: 38 | raise Exception('Account already exists') 39 | 40 | self.accounts[key] = 0.0 41 | return key 42 | 43 | def increment(self, key: str, quantity: float): 44 | if key not in self.accounts: 45 | raise Exception('Account not found') 46 | 47 | self.accounts[key] += quantity 48 | 49 | def transfer(self, source: str, dest: str, quantity: float): 50 | if source not in self.accounts: 51 | raise Exception('Source account not found') 52 | if dest not in self.accounts: 53 | raise Exception('Destination account not found') 54 | if self.accounts[source] < quantity: 55 | raise Exception('Not enough funds in source account') 56 | if quantity < 0: 57 | raise Exception('You cannot transfer negative currency') 58 | 59 | self.accounts[source] -= quantity 60 | self.accounts[dest] += quantity 61 | 62 | def balance(self, key: str): 63 | if key not in self.accounts: 64 | print(self.accounts) 65 | raise Exception('Account not found') 66 | 67 | return str(self.accounts[key]) 68 | 69 | 70 | There is no need to deal with the details now, but if you are familiar with 71 | Python you more or less understand where the thing is going. Once you have 72 | finished creating your smart contract, PyLedger can get it up and running in 73 | no time. 74 | 75 | .. code-block:: python 76 | 77 | from pyledger.server import run 78 | 79 | run(DigitalCurrency) 80 | 81 | Assume that the previous script is called *ledger.py*. Running the ledger is 82 | as simple as running the script with some options:: 83 | 84 | $> python ledger.py --sync 85 | 86 | Now you have your ledger up and running, you can connect to it with a REPL 87 | client:: 88 | 89 | $> pyledger-shell 90 | 91 | Connected to server: tcp:127.0.0.1:9000 92 | Pyledger REPL client, write 'help' for help or 'help command' for help on a specific command 93 | PL >>> help 94 | 95 | The Pyledger REPL is a console to interact with a Pyledger server. 96 | The list of available commands is the following 97 | 98 | help Shows this help 99 | disconnect Disconnects from the server in a clean way. 100 | contracts Lists the available contracts in the server 101 | api Shows the api for a particular contract 102 | call Calls a method of a contract 103 | broadcast Broadcast message all clients 104 | 105 | This client may have some limitations respect to a custom client. 106 | For instance, the server may push notifications to the clients, 107 | and using the client API, you could define callbacks to those 108 | pushed messages. 109 | 110 | Read the full documentation in http://pyledger.readthedocs.io 111 | 112 | PL >>> contracts 113 | ['DigitalCurrency'] 114 | PL >>> api DigitalCurrency 115 | {'add_account': {'key': }, 116 | 'balance': {'key': }, 117 | 'increment': {'key': , 'quantity': }, 118 | 'transfer': {'dest': , 119 | 'quantity': , 120 | 'source': }} 121 | PL >>> call DigitalCurrency add_account account1 122 | Call with pairs of key value arguments 123 | PL >>> call DigitalCurrency add_account key account1 124 | 'account1' 125 | PL >>> call DigitalCurrency increment key account1 quantity 100.0 126 | None 127 | PL >>> call DigitalCurrency balance key account1 128 | '100.0' 129 | PL >>> call DigitalCurrency add_account key account2 130 | 'account2' 131 | PL >>> call DigitalCurrency transfer source account1 dest account2 quantity 50.0 132 | None 133 | PL >>> call DigitalCurrency balance key account1 134 | '50.0' 135 | PL >>> call DigitalCurrency balance key account2 136 | '50.0' 137 | PL >>> disconnect 138 | Successfully closed, you can kill this with Ctrl-C 139 | WebSocket connection closed: 1000; None 140 | ^CBye 141 | 142 | 143 | Pyledger is possible thanks to `Autobahn `_ 144 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pyledger documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Mar 8 17:53:58 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.pardir, 23 | os.path.pardir))) 24 | 25 | __version__ = None 26 | with open('../../pyledger/__init__.py') as f: 27 | exec(f.read()) 28 | 29 | 30 | # -- General configuration ------------------------------------------------ 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # 34 | # needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = ['sphinx.ext.autodoc', 40 | 'sphinx.ext.intersphinx', 41 | 'sphinx.ext.mathjax', 42 | 'sphinx.ext.viewcode', 43 | 'sphinx.ext.githubpages'] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = 'pyledger' 59 | copyright = '2017, Guillem Borrell' 60 | author = 'Guillem Borrell' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = __version__ 68 | # The full version, including alpha/beta/rc tags. 69 | release = __version__ 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This patterns also effect to html_static_path and html_extra_path 81 | exclude_patterns = [] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # If true, `todo` and `todoList` produce output, else they produce nothing. 87 | todo_include_todos = False 88 | 89 | 90 | # -- Options for HTML output ---------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | # 95 | html_theme = 'sphinxdoc' 96 | 97 | # Theme options are theme-specific and customize the look and feel of a theme 98 | # further. For a list of options available for each theme, see the 99 | # documentation. 100 | # 101 | # html_theme_options = {} 102 | 103 | # Add any paths that contain custom static files (such as style sheets) here, 104 | # relative to this directory. They are copied after the builtin static files, 105 | # so a file named "default.css" will overwrite the builtin "default.css". 106 | html_static_path = ['_static'] 107 | 108 | 109 | # -- Options for HTMLHelp output ------------------------------------------ 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'pyledgerdoc' 113 | 114 | 115 | # -- Options for LaTeX output --------------------------------------------- 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'pyledger.tex', 'pyledger Documentation', 140 | 'Guillem Borrell', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output --------------------------------------- 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'pyledger', 'pyledger Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'pyledger', 'pyledger Documentation', 161 | author, 'pyledger', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | 167 | 168 | # Example configuration for intersphinx: refer to the Python standard library. 169 | intersphinx_mapping = {'https://docs.python.org/': None} 170 | -------------------------------------------------------------------------------- /tests/test_3_simple_contract.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from inspect import Signature, Parameter 18 | 19 | from pyledger.server.contract import SimpleContract, register_contract, \ 20 | methods, api, signatures, status 21 | from pyledger.server.status import BaseStatus 22 | 23 | 24 | def test_contract_status(): 25 | """Check if returns a status""" 26 | class MyContract(SimpleContract): 27 | counter = 0 28 | 29 | def greet(self, name: str): 30 | self.counter += 1 31 | return "hello, " + name 32 | 33 | this_contract = MyContract() 34 | this_contract_status = status(this_contract) 35 | 36 | assert isinstance(this_contract_status, BaseStatus) == True 37 | 38 | 39 | def test_full_contract(): 40 | """Check that a contract feels like a class""" 41 | class DigitalCurrency(SimpleContract): 42 | accounts = {} 43 | 44 | def add_account(self, key: str): 45 | if key in self.accounts: 46 | raise Exception('Account already exists') 47 | 48 | self.accounts[key] = 0.0 49 | 50 | def increment(self, key: str, quantity: float): 51 | if key not in self.accounts: 52 | raise Exception('Account not found') 53 | 54 | self.accounts[key] += quantity 55 | 56 | def transfer(self, source: str, dest: str, quantity: float): 57 | if source not in self.accounts: 58 | raise Exception('Source account not found') 59 | if dest not in self.accounts: 60 | raise Exception('Destination account not found') 61 | if self.accounts[source] < quantity: 62 | raise Exception('Not enough funds in source account') 63 | 64 | self.accounts[source] -= quantity 65 | self.accounts[dest] += quantity 66 | 67 | def balance(self, key: str): 68 | if key not in self.accounts: 69 | print(self.accounts) 70 | raise Exception('Account not found') 71 | 72 | return str(self.accounts[key]) 73 | 74 | contract = DigitalCurrency() 75 | 76 | assert [k for k in methods(contract)] == [ 77 | 'add_account', 'balance', 'increment', 'transfer'] 78 | 79 | assert api(contract) == { 80 | 'add_account': {'key': str}, 81 | 'balance': {'key': str}, 82 | 'increment': {'key': str, 'quantity': float}, 83 | 'transfer': {'dest': str, 'quantity': float, 'source': str} 84 | } 85 | 86 | assert signatures(contract) == { 87 | 'add_account': Signature(parameters=[ 88 | Parameter('key', Parameter.POSITIONAL_OR_KEYWORD, annotation=str)]), 89 | 'balance': Signature(parameters=[ 90 | Parameter('key', Parameter.POSITIONAL_OR_KEYWORD, annotation=str)]), 91 | 'increment': Signature(parameters=[ 92 | Parameter('key', Parameter.POSITIONAL_OR_KEYWORD, annotation=str), 93 | Parameter('quantity', Parameter.POSITIONAL_OR_KEYWORD, annotation=float) 94 | ]), 95 | 'transfer': Signature(parameters=[ 96 | Parameter('source', Parameter.POSITIONAL_OR_KEYWORD, annotation=str), 97 | Parameter('dest', Parameter.POSITIONAL_OR_KEYWORD, annotation=str), 98 | Parameter('quantity', Parameter.POSITIONAL_OR_KEYWORD, annotation=float) 99 | ]) 100 | } 101 | 102 | contract.add_account('key1') 103 | contract.increment('key1', 100.0) 104 | assert contract.balance('key1') == '100.0' 105 | 106 | 107 | def test_register_contract(): 108 | class MyContract(SimpleContract): 109 | counter = 0 110 | 111 | def greet(self, name: str): 112 | self.counter += 1 113 | return "hello, " + name 114 | 115 | this_contract = MyContract() 116 | register_contract(this_contract) 117 | 118 | assert True 119 | 120 | 121 | def test_register_larger_contract(): 122 | class DigitalCurrency(SimpleContract): 123 | accounts = {} 124 | 125 | def add_account(self, key: str): 126 | if key in self.accounts: 127 | raise Exception('Account already exists') 128 | 129 | self.accounts[key] = 0.0 130 | return key 131 | 132 | def increment(self, key: str, quantity: float): 133 | if key not in self.accounts: 134 | raise Exception('Account not found') 135 | 136 | self.accounts[key] += quantity 137 | 138 | def transfer(self, source: str, dest: str, quantity: float): 139 | if source not in self.accounts: 140 | raise Exception('Source account not found') 141 | if dest not in self.accounts: 142 | raise Exception('Destination account not found') 143 | if self.accounts[source] < quantity: 144 | raise Exception('Not enough funds in source account') 145 | 146 | self.accounts[source] -= quantity 147 | self.accounts[dest] += quantity 148 | 149 | def balance(self, key: str): 150 | if key not in self.accounts: 151 | print(self.accounts) 152 | raise Exception('Account not found') 153 | 154 | return str(self.accounts[key]) 155 | 156 | register_contract(DigitalCurrency()) 157 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Pyledger 2 | ======== 3 | 4 | **A simple ledger for smart contracts written in Python** 5 | 6 | .. image:: https://badge.fury.io/py/pyledger.svg 7 | :target: https://badge.fury.io/py/pyledger 8 | 9 | .. image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat 10 | :target: https://pyledger.readthedocs.io/en/latest 11 | 12 | .. image:: https://badge.fury.io/gh/guillemborrell%2Fpyledger.svg 13 | :target: https://badge.fury.io/gh/guillemborrell%2Fpyledger 14 | 15 | Smart contracts are taking over the financial ecosystem, but most platforms 16 | are terribly complicated given their parallel nature. What happens is that, 17 | if you don't need to deal with parallelism, building a ledger for smart 18 | contracts is relatively easy. Here's where Pyledger comes into play. 19 | 20 | Assume that you want to create a smart contract to implement a digital 21 | currency system. You have some features you consider necessary, namely 22 | creating accounts, adding currency to any account, checking the balance and 23 | transfer some amount. 24 | 25 | A smart contract is an application, so you need to code to create one. In 26 | Pyledger you can implement your smart contract in Python. In a few words, a 27 | smart contract in Pyledger is a Python class 28 | 29 | .. code-block:: python 30 | 31 | from pyledger.server.contract import SimpleContract 32 | 33 | class DigitalCurrency(SimpleContract): 34 | accounts = {} 35 | 36 | def add_account(self, key: str): 37 | if key in self.accounts: 38 | raise Exception('Account already exists') 39 | 40 | self.accounts[key] = 0.0 41 | return key 42 | 43 | def increment(self, key: str, quantity: float): 44 | if key not in self.accounts: 45 | raise Exception('Account not found') 46 | 47 | self.accounts[key] += quantity 48 | 49 | def transfer(self, source: str, dest: str, quantity: float): 50 | if source not in self.accounts: 51 | raise Exception('Source account not found') 52 | if dest not in self.accounts: 53 | raise Exception('Destination account not found') 54 | if self.accounts[source] < quantity: 55 | raise Exception('Not enough funds in source account') 56 | if quantity < 0: 57 | raise Exception('You cannot transfer negative currency') 58 | 59 | self.accounts[source] -= quantity 60 | self.accounts[dest] += quantity 61 | 62 | def balance(self, key: str): 63 | if key not in self.accounts: 64 | print(self.accounts) 65 | raise Exception('Account not found') 66 | 67 | return str(self.accounts[key]) 68 | 69 | 70 | There is no need to deal with the details now, but if you are familiar with 71 | Python you more or less understand where the thing is going. Once you have 72 | finished creating your smart contract, PyLedger can get it up and running in 73 | no time. 74 | 75 | .. code-block:: python 76 | 77 | from pyledger.server import run 78 | 79 | run(DigitalCurrency) 80 | 81 | Assume that the previous script is called *ledger.py*. Running the ledger is 82 | as simple as running the script with some options:: 83 | 84 | $> python ledger.py --sync 85 | 86 | Now you have your ledger up and running, you can connect to it with a REPL 87 | client:: 88 | 89 | $> pyledger-shell 90 | 91 | Connected to server: tcp:127.0.0.1:9000 92 | Pyledger REPL client, write 'help' for help or 'help command' for help on a specific command 93 | PL >>> help 94 | 95 | The Pyledger REPL is a console to interact with a Pyledger server. 96 | The list of available commands is the following 97 | 98 | help Shows this help 99 | disconnect Disconnects from the server in a clean way. 100 | contracts Lists the available contracts in the server 101 | api Shows the api for a particular contract 102 | call Calls a method of a contract 103 | broadcast Broadcast message all clients 104 | 105 | This client may have some limitations respect to a custom client. 106 | For instance, the server may push notifications to the clients, 107 | and using the client API, you could define callbacks to those 108 | pushed messages. 109 | 110 | Read the full documentation in http://pyledger.readthedocs.io 111 | 112 | PL >>> contracts 113 | ['DigitalCurrency'] 114 | PL >>> api DigitalCurrency 115 | {'add_account': {'key': }, 116 | 'balance': {'key': }, 117 | 'increment': {'key': , 'quantity': }, 118 | 'transfer': {'dest': , 119 | 'quantity': , 120 | 'source': }} 121 | PL >>> call DigitalCurrency add_account account1 122 | Call with pairs of key value arguments 123 | PL >>> call DigitalCurrency add_account key account1 124 | 'account1' 125 | PL >>> call DigitalCurrency increment key account1 quantity 100.0 126 | None 127 | PL >>> call DigitalCurrency balance key account1 128 | '100.0' 129 | PL >>> call DigitalCurrency add_account key account2 130 | 'account2' 131 | PL >>> call DigitalCurrency transfer source account1 dest account2 quantity 50.0 132 | None 133 | PL >>> call DigitalCurrency balance key account1 134 | '50.0' 135 | PL >>> call DigitalCurrency balance key account2 136 | '50.0' 137 | PL >>> disconnect 138 | Successfully closed, you can kill this with Ctrl-C 139 | WebSocket connection closed: 1000; None 140 | ^CBye 141 | 142 | 143 | Pyledger is possible thanks to `Autobahn `_ 144 | 145 | Now that we may have your attention, the actual docs. 146 | 147 | .. toctree:: 148 | :maxdepth: 2 149 | :caption: Contents: 150 | 151 | contract 152 | server 153 | chain 154 | users 155 | currency_example 156 | distributed 157 | 158 | 159 | Indices and tables 160 | ================== 161 | 162 | * :ref:`genindex` 163 | * :ref:`modindex` 164 | * :ref:`search` 165 | -------------------------------------------------------------------------------- /pyledger/server/db.py: -------------------------------------------------------------------------------- 1 | # Pyledger. A simple ledger for smart contracts implemented in Python 2 | # Copyright (C) 2017 Guillem Borrell Nogueras 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import base64 18 | from enum import Enum 19 | 20 | from cryptography.exceptions import InvalidKey 21 | from cryptography.hazmat.primitives import hashes 22 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 23 | from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, \ 24 | LargeBinary 25 | from sqlalchemy import create_engine, desc 26 | from sqlalchemy.ext.declarative import declarative_base 27 | from sqlalchemy.orm import relationship 28 | from sqlalchemy.orm import sessionmaker, scoped_session 29 | 30 | from pyledger.server.config import args 31 | from pyledger.server.config import password_backend, SECRET 32 | 33 | 34 | class Handler: 35 | def __init__(self): 36 | self.engine = create_engine(args.db, echo=args.debug) 37 | self.session = scoped_session(sessionmaker(bind=self.engine)) 38 | self.Model = declarative_base(bind=self.engine) 39 | 40 | def sync_tables(self): 41 | self.Model.metadata.create_all(self.engine) 42 | 43 | 44 | DB = Handler() 45 | Model = DB.Model 46 | 47 | 48 | # Now the models 49 | class Permissions(Enum): 50 | ROOT = 1 51 | USER = 2 52 | ANON = 3 53 | 54 | 55 | class Status(Model): 56 | __tablename__ = 'status' 57 | id = Column(Integer, primary_key=True) 58 | contract_id = Column(Integer, ForeignKey('contracts.id')) 59 | contract = relationship("Contract", back_populates="status") 60 | attributes = Column(LargeBinary) 61 | key = Column(LargeBinary, unique=True) # Crash if there is a key collision. 62 | when = Column(DateTime) 63 | owner = Column(String) 64 | 65 | def __repr__(self): 66 | return ''.format(self.key) 67 | 68 | @classmethod 69 | def query(cls): 70 | return DB.session.query(cls) 71 | 72 | 73 | class Contract(Model): 74 | __tablename__ = 'contracts' 75 | id = Column(Integer, primary_key=True) 76 | name = Column(String, unique=True) 77 | description = Column(String) 78 | created = Column(DateTime) 79 | status = relationship("Status", lazy="subquery") 80 | user_id = Column(Integer, ForeignKey('users.id')) 81 | user = relationship("User", back_populates="contracts") 82 | 83 | @classmethod 84 | def query(cls): 85 | return DB.session.query(cls) 86 | 87 | @staticmethod 88 | def from_name(name): 89 | return Contract.query().filter(Contract.name == name).one_or_none() 90 | 91 | def last_status(self): 92 | return Status.query().filter( 93 | Status.contract == self 94 | ).order_by(desc(Status.when)).first() 95 | 96 | def last_statuses(self): 97 | return Status.query().filter( 98 | Status.contract == self 99 | ).order_by(desc.Status.when).limit(2).all() 100 | 101 | 102 | class User(Model): 103 | __tablename__ = 'users' 104 | id = Column(Integer, primary_key=True) 105 | name = Column(String, unique=True) 106 | when = Column(DateTime) 107 | info = Column(LargeBinary) 108 | key = Column(String) 109 | password = Column(String) 110 | profile = Column(Integer) 111 | contracts = relationship("Contract", back_populates="user") 112 | sessions = relationship("Session", back_populates='user') 113 | 114 | def __repr__(self): 115 | return ''.format(self.name, self.key) 116 | 117 | def __str__(self): 118 | return ''.format(self.name, self.key) 119 | 120 | def set_password(self, password): 121 | self.password = base64.b64encode(password) 122 | 123 | def get_password(self): 124 | return base64.b64decode(self.password) 125 | 126 | def check_password(self, password): 127 | kpdf = PBKDF2HMAC( 128 | algorithm=hashes.SHA256(), 129 | length=32, 130 | salt=SECRET, 131 | iterations=1000000, 132 | backend=password_backend 133 | ) 134 | try: 135 | kpdf.verify(password.encode('utf-8'), self.get_password()) 136 | correct = True 137 | except InvalidKey as e: 138 | print(e) 139 | correct = False 140 | 141 | return correct 142 | 143 | @classmethod 144 | def query(cls): 145 | return DB.session.query(cls) 146 | 147 | @staticmethod 148 | def from_name(name): 149 | return User.query().filter(User.name == name).one_or_none() 150 | 151 | def get_permissions(self): 152 | return Permissions(self.profile) 153 | 154 | def set_permissions(self, permissions): 155 | self.profile = permissions.value 156 | 157 | 158 | class Session(Model): 159 | __tablename__ = 'sessions' 160 | id = Column(Integer, primary_key=True) 161 | key = Column(String, unique=True) 162 | registered = Column(DateTime) 163 | until = Column(DateTime) 164 | user_id = Column(Integer, ForeignKey('users.id')) 165 | user = relationship("User", back_populates="sessions") 166 | 167 | def __repr__(self): 168 | return 'Session {}'.format(self.key) 169 | 170 | @classmethod 171 | def query(cls): 172 | return DB.session.query(cls) 173 | 174 | @staticmethod 175 | def from_key(key): 176 | return Session.query().filter(Session.key == key).one_or_none() 177 | 178 | 179 | class Task(Model): 180 | __tablename__ = 'tasks' 181 | id = Column(Integer, primary_key=True) 182 | method = Column(String) 183 | when = Column(DateTime) 184 | 185 | def __repr__(self): 186 | return 'Task. Scheduled: {}'.format(self.when.isoformat()) 187 | -------------------------------------------------------------------------------- /docs/source/contract.rst: -------------------------------------------------------------------------------- 1 | How to create a smart contract 2 | ============================== 3 | 4 | A smart contract in pyledger is a function that returns an instance of 5 | :py:class:`pyledger.contract.Builder`. This object is a helper to manage the 6 | attributes of the smart contract and the methods that may or may not modify 7 | those attributes. The simplest smart contract you may think of is one that 8 | just returns the string "Hello". 9 | 10 | .. code-block:: python 11 | 12 | from pyledger.handlers import make_tornado 13 | from pyledger.contract import Builder 14 | from pyledger.config import args 15 | import tornado.ioloop 16 | 17 | 18 | def hello(): 19 | def say_hello(attrs): 20 | return attrs, 'Hello' 21 | 22 | contract = Builder('Hello') 23 | contract.add_method(say_hello) 24 | 25 | return contract 26 | 27 | 28 | if __name__ == '__main__': 29 | application = make_tornado(hello) 30 | application.listen(args.port) 31 | tornado.ioloop.IOLoop.instance().start() 32 | 33 | If you run this snippet as script without options, you will be able to 34 | connect to this server with the command line client provided by pyledger, 35 | called ``pyledger-shell``:: 36 | 37 | (env) $> pyledger-shell 38 | PyLedger simple client 39 | (http://localhost:8888)> contracts 40 | Hello 41 | (http://localhost:8888)> api Hello 42 | say_hello ( ) 43 | 44 | (http://localhost:8888)> call Hello say_hello 45 | Hello 46 | (http://localhost:8888)> 47 | 48 | 49 | This almost trival example is useful to understand the very basics about how 50 | the contracts are created. The contract is called *Hello* which is the argument 51 | of the Builder instance. The method *say_hello* gets no arguments and it 52 | modifies no attributes, but it must get the attributes as an argument and 53 | return them anyways. If an additional argument, like the ``Hello`` string, 54 | is returned by the method, it is given as a second return argument. 55 | 56 | Attributes 57 | ---------- 58 | 59 | Let's change the previous example a little by adding an attribute to the 60 | contract. For instance, we will make a counter of the amount of times the 61 | contract has greeted us. 62 | 63 | .. code-block:: python 64 | 65 | def hello(): 66 | def say_hello(attrs): 67 | attrs.counter += 1 68 | return attrs, 'Hello {}'.format(attrs.counter) 69 | 70 | contract = Builder('Hello') 71 | contract.add_attribute('counter', 0) 72 | contract.add_method(say_hello) 73 | 74 | return contract 75 | 76 | A session with this new smart contract would be as follows:: 77 | 78 | (http://localhost:8888)> call Hello say_hello 79 | Hello 1 80 | (http://localhost:8888)> call Hello say_hello 81 | Hello 2 82 | (http://localhost:8888)> status Hello 83 | {'counter': 2} 84 | 85 | 86 | Note that the contract function pretty much looks like an object, it has 87 | attributes and methods that change those attributes. It is also quite similar 88 | as how Solidity defines the smart contracts, with attributes and 89 | methods that modify them. Pyledger is a little more explicit. 90 | 91 | We can also define methods with arguments, and here's one of the important 92 | particularities of pyledger: *all the arguments but the first one (attrs) 93 | must be type annotated*. For instance, this is a contract that greets with a 94 | name, that is passed as a parameter. 95 | 96 | .. code-block:: python 97 | 98 | def hello(): 99 | def say_hello(attrs, name: str): 100 | attrs.counter += 1 101 | return attrs, 'Hello {} for time #{}'.format(name, attrs.counter) 102 | 103 | contract = Builder('Hello') 104 | contract.add_attribute('counter', 0) 105 | contract.add_method(say_hello) 106 | 107 | return contract 108 | 109 | 110 | A smart contract must expose an API, and type annotation is needed to let the 111 | client and any user of the contract to know which type the arguments must be:: 112 | 113 | (env) $> pyledger-shell 114 | PyLedger simple client 115 | (http://localhost:8888)> api Hello 116 | say_hello ( name [str] ) 117 | 118 | (http://localhost:8888)> call Hello say_hello Guillem 119 | Hello Guillem for time #1 120 | (http://localhost:8888)> call Hello say_hello Guillem 121 | Hello Guillem for time #2 122 | (http://localhost:8888)> status Hello 123 | {'counter': 2} 124 | (http://localhost:8888)> 125 | 126 | 127 | With these features, the smart contracts can be as complex as needed. One can 128 | store information of any kind within the arguments, that are the ones that 129 | define the status of the contract. 130 | 131 | .. important:: 132 | 133 | If you want the contract to be fast and you want to avoid obscure bugs 134 | too, keep your attributes as primitive python types. 135 | 136 | 137 | Exceptions 138 | ---------- 139 | 140 | Contracts can raise only a generic exception of type :py:class:`Exception`. 141 | The goal is only to inform the user that the operation has not been 142 | successful. Note that the methods that return no additional value send back 143 | to the client the string *SUCCESS*. This means that the client is always 144 | waiting for a message to come. 145 | 146 | We will introduce some very simple exception that checks the most common 147 | mispelling of my name 148 | 149 | .. code-block:: python 150 | 151 | def hello(): 152 | def say_hello(attrs, name: str): 153 | if name == 'Guillen': 154 | raise Exception('You probably mispelled Guillem') 155 | 156 | attrs.counter += 1 157 | return attrs, 'Hello {} for time #{}'.format(name, attrs.counter) 158 | 159 | contract = Builder('Hello') 160 | contract.add_attribute('counter', 0) 161 | contract.add_method(say_hello) 162 | 163 | return contract 164 | 165 | And how the exception is handled at the client side:: 166 | 167 | (env) $> pyledger-shell 168 | PyLedger simple client 169 | (http://localhost:8888)> call Hello say_hello Guillem 170 | Hello Guillem for time #1 171 | (http://localhost:8888)> call Hello say_hello Guillen 172 | You probably mispelled Guillem 173 | (http://localhost:8888)> call Hello say_hello Guillem 174 | Hello Guillem for time #2 175 | 176 | 177 | Docstrings of the classes cited in this section 178 | ----------------------------------------------- 179 | 180 | .. autoclass:: pyledger.contract.Builder 181 | :members: add_method, add_attribute 182 | -------------------------------------------------------------------------------- /pyledger/pyledger_message_pb2.py: -------------------------------------------------------------------------------- 1 | # Generated by the protocol buffer compiler. DO NOT EDIT! 2 | # source: pyledger_message.proto 3 | 4 | import sys 5 | _b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import message as _message 8 | from google.protobuf import reflection as _reflection 9 | from google.protobuf import symbol_database as _symbol_database 10 | from google.protobuf import descriptor_pb2 11 | # @@protoc_insertion_point(imports) 12 | 13 | _sym_db = _symbol_database.Default() 14 | 15 | 16 | 17 | 18 | DESCRIPTOR = _descriptor.FileDescriptor( 19 | name='pyledger_message.proto', 20 | package='', 21 | syntax='proto3', 22 | serialized_pb=_b('\n\x16pyledger_message.proto\"\xa8\x01\n\x0fPyledgerRequest\x12\x0f\n\x07request\x18\x01 \x01(\t\x12\x10\n\x08\x63ontract\x18\x02 \x01(\t\x12\x0c\n\x04\x63\x61ll\x18\x03 \x01(\t\x12\x12\n\nclient_key\x18\x04 \x01(\t\x12\x13\n\x0bsession_key\x18\x05 \x01(\t\x12\x0c\n\x04user\x18\x06 \x01(\t\x12\x10\n\x08password\x18\x07 \x01(\t\x12\r\n\x05topic\x18\x08 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\t \x01(\x0c\"\xa1\x01\n\x10PyledgerResponse\x12\x10\n\x08response\x18\x01 \x01(\t\x12\x10\n\x08\x63ontract\x18\x02 \x01(\t\x12\x12\n\nsuccessful\x18\x03 \x01(\x08\x12\r\n\x05topic\x18\x04 \x01(\t\x12\x10\n\x08\x61uth_key\x18\x05 \x01(\t\x12\x13\n\x0bsession_key\x18\x06 \x01(\t\x12\x11\n\ttimestamp\x18\x07 \x01(\x02\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x62\x06proto3') 23 | ) 24 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 25 | 26 | 27 | 28 | 29 | _PYLEDGERREQUEST = _descriptor.Descriptor( 30 | name='PyledgerRequest', 31 | full_name='PyledgerRequest', 32 | filename=None, 33 | file=DESCRIPTOR, 34 | containing_type=None, 35 | fields=[ 36 | _descriptor.FieldDescriptor( 37 | name='request', full_name='PyledgerRequest.request', index=0, 38 | number=1, type=9, cpp_type=9, label=1, 39 | has_default_value=False, default_value=_b("").decode('utf-8'), 40 | message_type=None, enum_type=None, containing_type=None, 41 | is_extension=False, extension_scope=None, 42 | options=None), 43 | _descriptor.FieldDescriptor( 44 | name='contract', full_name='PyledgerRequest.contract', index=1, 45 | number=2, type=9, cpp_type=9, label=1, 46 | has_default_value=False, default_value=_b("").decode('utf-8'), 47 | message_type=None, enum_type=None, containing_type=None, 48 | is_extension=False, extension_scope=None, 49 | options=None), 50 | _descriptor.FieldDescriptor( 51 | name='call', full_name='PyledgerRequest.call', index=2, 52 | number=3, type=9, cpp_type=9, label=1, 53 | has_default_value=False, default_value=_b("").decode('utf-8'), 54 | message_type=None, enum_type=None, containing_type=None, 55 | is_extension=False, extension_scope=None, 56 | options=None), 57 | _descriptor.FieldDescriptor( 58 | name='client_key', full_name='PyledgerRequest.client_key', index=3, 59 | number=4, type=9, cpp_type=9, label=1, 60 | has_default_value=False, default_value=_b("").decode('utf-8'), 61 | message_type=None, enum_type=None, containing_type=None, 62 | is_extension=False, extension_scope=None, 63 | options=None), 64 | _descriptor.FieldDescriptor( 65 | name='session_key', full_name='PyledgerRequest.session_key', index=4, 66 | number=5, type=9, cpp_type=9, label=1, 67 | has_default_value=False, default_value=_b("").decode('utf-8'), 68 | message_type=None, enum_type=None, containing_type=None, 69 | is_extension=False, extension_scope=None, 70 | options=None), 71 | _descriptor.FieldDescriptor( 72 | name='user', full_name='PyledgerRequest.user', index=5, 73 | number=6, type=9, cpp_type=9, label=1, 74 | has_default_value=False, default_value=_b("").decode('utf-8'), 75 | message_type=None, enum_type=None, containing_type=None, 76 | is_extension=False, extension_scope=None, 77 | options=None), 78 | _descriptor.FieldDescriptor( 79 | name='password', full_name='PyledgerRequest.password', index=6, 80 | number=7, type=9, cpp_type=9, label=1, 81 | has_default_value=False, default_value=_b("").decode('utf-8'), 82 | message_type=None, enum_type=None, containing_type=None, 83 | is_extension=False, extension_scope=None, 84 | options=None), 85 | _descriptor.FieldDescriptor( 86 | name='topic', full_name='PyledgerRequest.topic', index=7, 87 | number=8, type=9, cpp_type=9, label=1, 88 | has_default_value=False, default_value=_b("").decode('utf-8'), 89 | message_type=None, enum_type=None, containing_type=None, 90 | is_extension=False, extension_scope=None, 91 | options=None), 92 | _descriptor.FieldDescriptor( 93 | name='data', full_name='PyledgerRequest.data', index=8, 94 | number=9, type=12, cpp_type=9, label=1, 95 | has_default_value=False, default_value=_b(""), 96 | message_type=None, enum_type=None, containing_type=None, 97 | is_extension=False, extension_scope=None, 98 | options=None), 99 | ], 100 | extensions=[ 101 | ], 102 | nested_types=[], 103 | enum_types=[ 104 | ], 105 | options=None, 106 | is_extendable=False, 107 | syntax='proto3', 108 | extension_ranges=[], 109 | oneofs=[ 110 | ], 111 | serialized_start=27, 112 | serialized_end=195, 113 | ) 114 | 115 | 116 | _PYLEDGERRESPONSE = _descriptor.Descriptor( 117 | name='PyledgerResponse', 118 | full_name='PyledgerResponse', 119 | filename=None, 120 | file=DESCRIPTOR, 121 | containing_type=None, 122 | fields=[ 123 | _descriptor.FieldDescriptor( 124 | name='response', full_name='PyledgerResponse.response', index=0, 125 | number=1, type=9, cpp_type=9, label=1, 126 | has_default_value=False, default_value=_b("").decode('utf-8'), 127 | message_type=None, enum_type=None, containing_type=None, 128 | is_extension=False, extension_scope=None, 129 | options=None), 130 | _descriptor.FieldDescriptor( 131 | name='contract', full_name='PyledgerResponse.contract', index=1, 132 | number=2, type=9, cpp_type=9, label=1, 133 | has_default_value=False, default_value=_b("").decode('utf-8'), 134 | message_type=None, enum_type=None, containing_type=None, 135 | is_extension=False, extension_scope=None, 136 | options=None), 137 | _descriptor.FieldDescriptor( 138 | name='successful', full_name='PyledgerResponse.successful', index=2, 139 | number=3, type=8, cpp_type=7, label=1, 140 | has_default_value=False, default_value=False, 141 | message_type=None, enum_type=None, containing_type=None, 142 | is_extension=False, extension_scope=None, 143 | options=None), 144 | _descriptor.FieldDescriptor( 145 | name='topic', full_name='PyledgerResponse.topic', index=3, 146 | number=4, type=9, cpp_type=9, label=1, 147 | has_default_value=False, default_value=_b("").decode('utf-8'), 148 | message_type=None, enum_type=None, containing_type=None, 149 | is_extension=False, extension_scope=None, 150 | options=None), 151 | _descriptor.FieldDescriptor( 152 | name='auth_key', full_name='PyledgerResponse.auth_key', index=4, 153 | number=5, type=9, cpp_type=9, label=1, 154 | has_default_value=False, default_value=_b("").decode('utf-8'), 155 | message_type=None, enum_type=None, containing_type=None, 156 | is_extension=False, extension_scope=None, 157 | options=None), 158 | _descriptor.FieldDescriptor( 159 | name='session_key', full_name='PyledgerResponse.session_key', index=5, 160 | number=6, type=9, cpp_type=9, label=1, 161 | has_default_value=False, default_value=_b("").decode('utf-8'), 162 | message_type=None, enum_type=None, containing_type=None, 163 | is_extension=False, extension_scope=None, 164 | options=None), 165 | _descriptor.FieldDescriptor( 166 | name='timestamp', full_name='PyledgerResponse.timestamp', index=6, 167 | number=7, type=2, cpp_type=6, label=1, 168 | has_default_value=False, default_value=float(0), 169 | message_type=None, enum_type=None, containing_type=None, 170 | is_extension=False, extension_scope=None, 171 | options=None), 172 | _descriptor.FieldDescriptor( 173 | name='data', full_name='PyledgerResponse.data', index=7, 174 | number=8, type=12, cpp_type=9, label=1, 175 | has_default_value=False, default_value=_b(""), 176 | message_type=None, enum_type=None, containing_type=None, 177 | is_extension=False, extension_scope=None, 178 | options=None), 179 | ], 180 | extensions=[ 181 | ], 182 | nested_types=[], 183 | enum_types=[ 184 | ], 185 | options=None, 186 | is_extendable=False, 187 | syntax='proto3', 188 | extension_ranges=[], 189 | oneofs=[ 190 | ], 191 | serialized_start=198, 192 | serialized_end=359, 193 | ) 194 | 195 | DESCRIPTOR.message_types_by_name['PyledgerRequest'] = _PYLEDGERREQUEST 196 | DESCRIPTOR.message_types_by_name['PyledgerResponse'] = _PYLEDGERRESPONSE 197 | 198 | PyledgerRequest = _reflection.GeneratedProtocolMessageType('PyledgerRequest', (_message.Message,), dict( 199 | DESCRIPTOR = _PYLEDGERREQUEST, 200 | __module__ = 'pyledger_message_pb2' 201 | # @@protoc_insertion_point(class_scope:PyledgerRequest) 202 | )) 203 | _sym_db.RegisterMessage(PyledgerRequest) 204 | 205 | PyledgerResponse = _reflection.GeneratedProtocolMessageType('PyledgerResponse', (_message.Message,), dict( 206 | DESCRIPTOR = _PYLEDGERRESPONSE, 207 | __module__ = 'pyledger_message_pb2' 208 | # @@protoc_insertion_point(class_scope:PyledgerResponse) 209 | )) 210 | _sym_db.RegisterMessage(PyledgerResponse) 211 | 212 | 213 | # @@protoc_insertion_point(module_scope) 214 | -------------------------------------------------------------------------------- /pyledger/server/handlers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import hashlib 3 | import inspect 4 | import pickle 5 | from typing import Tuple 6 | from uuid import uuid4 7 | 8 | from google.protobuf.message import DecodeError 9 | 10 | from pyledger.pyledger_message_pb2 import PyledgerRequest, PyledgerResponse 11 | from pyledger.server.auth import allow, permissions_registry, create_user, method_permissions_registry 12 | from pyledger.server.config import LIFETIME 13 | from pyledger.server.contract import contract_registry, api, methods 14 | from pyledger.server.db import Permissions, User, DB, Session, Contract, Status 15 | 16 | 17 | class Handler: 18 | @allow(Permissions.ROOT) 19 | def new_user(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 20 | """ 21 | Request an activation key 22 | 23 | :param message: 24 | :return: 25 | """ 26 | name, password = pickle.loads(message.data) 27 | create_user(name, password) 28 | 29 | return True, name.encode('utf-8') 30 | 31 | @allow(Permissions.USER) 32 | def set_password(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 33 | password = message.data.decode('utf-8') 34 | user = User.from_name(message.user) 35 | user.set_password(password) 36 | DB.session.commit() 37 | 38 | return True, message.user.encode('utf-8') 39 | 40 | def api(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 41 | """Get the api of the contract""" 42 | 43 | if message.contract not in contract_registry: 44 | return False, 'User function {} not present'.format( 45 | message.contract).encode('utf-8') 46 | 47 | return True, pickle.dumps(api(contract_registry[message.contract])) 48 | 49 | def session(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 50 | """ 51 | Get authentication key 52 | 53 | :param message: 54 | :return: 55 | """ 56 | user = User.from_name(message.user) 57 | session = Session() 58 | session.user = user 59 | session.key = str(uuid4()) 60 | session.registered = datetime.datetime.now() 61 | session.until = datetime.datetime.now() + datetime.timedelta(hours=LIFETIME) 62 | 63 | DB.session.add(session) 64 | DB.session.commit() 65 | 66 | return True, session.key.encode('utf-8') 67 | 68 | def echo(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 69 | """ 70 | An echo handler for testing authentication and authorization. 71 | 72 | :param message: Request from the client 73 | :return: 74 | """ 75 | return True, b'echo' 76 | 77 | def contracts(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 78 | """ 79 | Returns a serialized list of the available contracts 80 | 81 | :param message: Request from the client 82 | :return: 83 | """ 84 | return True, pickle.dumps([k for k in contract_registry]) 85 | 86 | def status(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 87 | if message.contract not in contract_registry: 88 | return False, 'User function {} not present'.format( 89 | message.contract).encode('utf-8') 90 | 91 | contract_class = contract_registry[message.contract] 92 | status_instance = contract_class._status_class() 93 | contract = Contract.from_name(message.contract) 94 | status = contract.last_status() 95 | status_instance.load(status.attributes) 96 | return True, pickle.dumps(status_instance.to_dict()) 97 | 98 | def verify(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 99 | pass 100 | 101 | def call(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 102 | """ 103 | Call handler for contract methods. 104 | 105 | :param message: 106 | :return: 107 | """ 108 | if message.contract not in contract_registry: 109 | return False, 'Contract {} not available'.format( 110 | message.contract).encode('utf-8') 111 | 112 | contract = contract_registry[message.contract] 113 | if message.call not in methods(contract): 114 | return False, 'Method {} not found in contact'.format( 115 | message.call).encode('utf-8') 116 | 117 | if message.call in method_permissions_registry: 118 | user = User.from_name(message.user) 119 | permission_required = method_permissions_registry[message.call] 120 | 121 | if not user: 122 | if permission_required.value < Permissions.ANON.value: 123 | return False, b'Not enough permissions' 124 | 125 | elif not user.check_password(message.password): 126 | return False, b'Wrong user and/or password' 127 | 128 | elif user.get_permissions().value > permission_required.value: 129 | return False, b'Not enough permissions' 130 | 131 | # Get the last status of the contract. 132 | db_contract = Contract.from_name(message.contract) 133 | status_data = db_contract.last_status() 134 | status = contract._status_class() 135 | status.load(status_data.attributes) 136 | 137 | method = contract.__class__.__dict__[message.call] 138 | method_args = pickle.loads(message.data) 139 | 140 | # Coerce types given the API, since the arguments are pickled 141 | method_api = api(contract_registry[message.contract]) 142 | signature = method_api[message.call] 143 | 144 | for arg in method_args: 145 | try: 146 | method_args[arg] = signature[arg](method_args[arg]) 147 | except KeyError: 148 | return False, str( 149 | ValueError('{} is not a valid key'.format(arg))).encode() 150 | 151 | # Load additional attributes 152 | status.user = message.user 153 | status.session = message.session_key 154 | 155 | # Call the method 156 | result = method(status, **method_args) 157 | 158 | # Persist the new status 159 | new_status = Status() 160 | new_status.contract = db_contract 161 | new_status.attributes = status.dump() 162 | new_status.when = datetime.datetime.now() 163 | 164 | # This is the status chain standard 165 | m = hashlib.sha256() 166 | m.update(status_data.key) 167 | m.update(new_status.when.isoformat().encode('utf-8')) 168 | m.update(new_status.attributes) 169 | 170 | new_status.key = m.digest() 171 | DB.session.add(new_status) 172 | DB.session.commit() 173 | 174 | return True, pickle.dumps(result) 175 | 176 | def broadcast(self, message: PyledgerRequest) -> Tuple[bool, bytes]: 177 | print('BROADCAST', pickle.loads(message.data)) 178 | return True, message.data 179 | 180 | 181 | def handler_methods(handler): 182 | """ 183 | Obtain the methods of the handler. Utility funciton in case some 184 | method is added sometime in the future. 185 | 186 | :param handler: 187 | :return: 188 | """ 189 | member_list = inspect.getmembers(handler, predicate=inspect.ismethod) 190 | return [pair[0] for pair in member_list] 191 | 192 | 193 | def handle_request(payload: bytes): 194 | """ 195 | Handle a single request 196 | 197 | :param payload: Serialized PyledgerRequest message 198 | :return: 199 | """ 200 | handler = Handler() 201 | message = PyledgerRequest() 202 | response = PyledgerResponse() 203 | 204 | try: 205 | message.ParseFromString(payload) 206 | except DecodeError: 207 | response.successful = False 208 | response.data = b'Message not properly formatted' 209 | return response.SerializeToString() 210 | 211 | if message.request not in handler_methods(handler): 212 | response.successful = False 213 | response.data = 'Request type {} not available'.format(message.request).encode() 214 | return response.SerializeToString() 215 | 216 | else: 217 | # Handle authentication 218 | if message.request in permissions_registry: 219 | user = User.from_name(message.user) 220 | permission_required = permissions_registry[message.request] 221 | 222 | if not user.check_password(message.password): 223 | response.successful = False 224 | response.data = b'Wrong user and/or password' 225 | return response.SerializeToString() 226 | 227 | if user.get_permissions().value > permission_required.value: 228 | response.successful = False 229 | response.data = b'Not enough permissions' 230 | return response.SerializeToString() 231 | 232 | session = Session.from_key(message.session_key) 233 | 234 | if not session: 235 | response.successful = False 236 | response.data = b'Session not available' 237 | return response.SerializeToString() 238 | 239 | if not session.user == user: 240 | response.successful = False 241 | response.data = b'Session not owned by this user' 242 | return response.SerializeToString() 243 | 244 | if session.until < datetime.datetime.now(): 245 | response.successful = False 246 | response.data = b'Session expired, restart your client' 247 | return response.SerializeToString() 248 | 249 | # Select the function from the handler 250 | try: 251 | print('Handling message', message) 252 | successful, result = getattr(handler, message.request)(message) 253 | except Exception as exc: 254 | successful = False 255 | result = b'Exception in user function: ' + repr(exc).encode('utf-8') 256 | 257 | response.successful = successful 258 | response.data = result 259 | return response.SerializeToString() 260 | 261 | 262 | def make_server(): 263 | """ 264 | Create a server given a smart contract. 265 | :param contract: 266 | :return: 267 | """ 268 | pass -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------