├── src ├── __init__.py ├── utils │ ├── __init__.py │ ├── encode_keys.py │ ├── logger.py │ ├── secp256k1.py │ ├── dataclass_json.py │ ├── storage.py │ ├── utils.py │ ├── constants.py │ └── dataclass_json_core.py ├── db │ └── .gitignore ├── log │ └── .gitignore ├── uuids.json ├── views │ ├── footer.html │ ├── info.html │ ├── block.html │ ├── account.html │ ├── transaction.html │ ├── chains.html │ ├── header.html │ ├── index.html │ ├── about.html │ ├── wallet.html │ ├── error.html │ └── explorer.html ├── static │ ├── favicon.ico │ ├── img │ │ ├── vjcoin.png │ │ └── vjtichain.png │ └── css │ │ └── vis.css ├── authority_rules.py ├── authority_rules.json ├── block_miner_test.py ├── client.py ├── dns_seed.py ├── transaction_creator.py ├── wallet.py ├── authority.py ├── miner.py ├── block_creator.py ├── wallet.java ├── Validation Rules.md ├── core.py └── fullnode.py ├── .gitattributes ├── ddoskill.conf ├── jail.local ├── structure.txt ├── LICENSE ├── environment.yml ├── .gitignore └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/db/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/log/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /src/uuids.json: -------------------------------------------------------------------------------- 1 | ["abcde", "vwxyz", "12345"] 2 | -------------------------------------------------------------------------------- /src/views/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/static/* linguist-vendored 2 | -------------------------------------------------------------------------------- /ddoskill.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | failregex = : Called function .* 3 | ignoreregex = 4 | 5 | -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJTI-AI-Blockchain/vjtichain/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/img/vjcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJTI-AI-Blockchain/vjtichain/HEAD/src/static/img/vjcoin.png -------------------------------------------------------------------------------- /src/static/img/vjtichain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VJTI-AI-Blockchain/vjtichain/HEAD/src/static/img/vjtichain.png -------------------------------------------------------------------------------- /src/authority_rules.py: -------------------------------------------------------------------------------- 1 | import json 2 | import utils.constants as consts 3 | 4 | # Retrieve Authority List 5 | with open(consts.AUTHORITY_RULES_LOC, "r") as file: 6 | data = file.read() 7 | authority_rules = json.loads(data) 8 | -------------------------------------------------------------------------------- /jail.local: -------------------------------------------------------------------------------- 1 | [ddoskill] 2 | enabled = true 3 | port = 1000:10000 4 | filter = ddoskill 5 | action = iptables-allports[name=fail2ban] 6 | logpath = /home/memes/Desktop/vjtichain/src/log/ip.log 7 | findtime = 15 8 | bantime = 60 9 | maxretry = 60 10 | -------------------------------------------------------------------------------- /src/utils/encode_keys.py: -------------------------------------------------------------------------------- 1 | from fastecdsa.asn1 import decode_key, encode_public_key as encode_pub_key_point 2 | from binascii import a2b_base64 3 | 4 | 5 | def decode_public_key(base64_key: str): 6 | raw_data = a2b_base64(base64_key) 7 | return decode_key(raw_data)[1] 8 | 9 | 10 | def encode_public_key(pub_key_point): 11 | encoded_key = encode_pub_key_point(pub_key_point) 12 | base64_key = "".join(encoded_key.split("\n")[1:-1]) 13 | return base64_key 14 | -------------------------------------------------------------------------------- /src/views/info.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="info") 2 | 3 |
4 | % for key, val in data.items(): 5 |
6 |
7 |
{{ key }}
8 |
9 |
10 |

{{ val }}

11 |
12 |
13 | % end 14 |
15 | 16 | % include('footer.html') -------------------------------------------------------------------------------- /structure.txt: -------------------------------------------------------------------------------- 1 | src 2 | ├── db 3 | │   └── 9000block.sqlite # Local DB File 4 | ├── log 5 | │   └── 12 Dec 17:11:51.log # Log file for the full node 6 | ├── utils 7 | │   ├── constants.py # Blockchain Configuration 8 | │   ├── encode_keys.py # Handle key encoding and decoding 9 | │   ├── logger.py # Code for Logging 10 | │   ├── secp256k1.py # ECDSA code 11 | │   ├── storage.py # Handle block storage 12 | │   └── utils.py # Utility functions 13 | ├── wallet 14 | │   ├── 9000.key # Private key for full node 15 | │   └── 9000.pub # Public key for full node 16 | ├── core.py # code for core classes of Blockchain 17 | ├── dns_seed.py # code for DNS seed 18 | ├── fullnode.py # Code for the Full node 19 | ├── miner.py # Functions for mining 20 | └── wallet.py # Code for handling public and private keys 21 | -------------------------------------------------------------------------------- /src/authority_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "authorities": [ 3 | { 4 | "name": "Node 0", 5 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0tmjDG6v51ELMieRGuTfOgmfTe7BzNBsHQseqygX58+MQjNyjoOPkphghhYFpIFPzVORAI6Qief9lrncuWsOMg==", 6 | "from": 0, 7 | "to": 28795 8 | }, 9 | { 10 | "name": "Node 1", 11 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0tmjDG6v51ELMieRGuTfOgmfTe7BzNBsHQseqygX58+MQjNyjoOPkphghhYFpIFPzVORAI6Qief9lrncuWsOMg==", 12 | "from": 28800, 13 | "to": 57600 14 | }, 15 | { 16 | "name": "Node 0", 17 | "pubkey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0tmjDG6v51ELMieRGuTfOgmfTe7BzNBsHQseqygX58+MQjNyjoOPkphghhYFpIFPzVORAI6Qief9lrncuWsOMg==", 18 | "from": 57605, 19 | "to": 86395 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/block_miner_test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from core import genesis_block, genesis_block_header 4 | from utils.utils import dhash 5 | 6 | 7 | def is_proper_difficulty(target_difficulty, bhash: str) -> bool: 8 | pow = 0 9 | for c in bhash: 10 | if not c == "0": 11 | break 12 | else: 13 | pow += 1 14 | if pow < target_difficulty: 15 | return False 16 | return True 17 | 18 | 19 | print(genesis_block, dhash(genesis_block_header)) 20 | 21 | for difficulty in range(5, 10): 22 | tss = time.time() 23 | for n in range(2 ** 64): 24 | genesis_block_header.nonce = n 25 | genesis_block_header.target_difficulty = difficulty 26 | bhash = dhash(genesis_block_header) 27 | if is_proper_difficulty(difficulty, bhash): 28 | print(f"Timestamp {int(tss)} Nonce {n} hash {bhash}\n Difficulty {difficulty} in {(time.time() - tss)} secs") 29 | print(genesis_block_header) 30 | DONE = True 31 | break 32 | if not DONE: 33 | print("Miner: Exhausted all 2 ** 64 values without finding proper hash") 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sanket Shanbhag 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | from . import constants as consts 5 | 6 | for name in ['werkzeug', 'bottle', 'waitress']: 7 | log = logging.getLogger(name) 8 | log.setLevel(logging.CRITICAL) 9 | log.disabled = True 10 | 11 | logger = logging.getLogger("vjtichain") 12 | logger.propagate = False 13 | 14 | iplogger = logging.getLogger("ipd") 15 | iplogger.propagate = False 16 | iplogger.setLevel(logging.DEBUG) 17 | ipformatter = logging.Formatter("%(asctime)s %(message)s", consts.DATE_FORMAT) 18 | ipfile_handler = logging.FileHandler(consts.LOG_DIRECTORY + "ip.log") 19 | ipfile_handler.setFormatter(ipformatter) 20 | ipfile_handler.setLevel(logging.DEBUG) 21 | 22 | formatter = logging.Formatter("%(asctime)s %(levelname)-10s %(message)s", consts.DATE_FORMAT) 23 | logger.setLevel(logging.DEBUG) 24 | 25 | file_handler = logging.FileHandler(consts.LOG_DIRECTORY + datetime.strftime(datetime.now(), consts.DATE_FORMAT) + ".log") 26 | file_handler.setFormatter(formatter) 27 | file_handler.setLevel(logging.DEBUG) 28 | 29 | stream_handler = logging.StreamHandler() 30 | stream_handler.setFormatter(formatter) 31 | stream_handler.setLevel(consts.LOG_LEVEL) 32 | 33 | logger.addHandler(file_handler) 34 | logger.addHandler(stream_handler) 35 | 36 | iplogger.addHandler(ipfile_handler) -------------------------------------------------------------------------------- /src/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: 3 | Functions for interacting with api's 4 | Send and receive coins 5 | """ 6 | 7 | import hashlib 8 | import secrets 9 | from typing import Tuple 10 | 11 | from utils.secp256k1 import b58_encode, point_mul 12 | 13 | 14 | class Wallet: 15 | def __init__(self): 16 | self.private_key = self.create_private_key() 17 | 18 | @staticmethod 19 | def create_private_key() -> bytes: 20 | return secrets.token_bytes(32) 21 | 22 | # See chp. 4 of Mastering Bitcoin 23 | def generate_address(self) -> Tuple[str, str]: 24 | q = point_mul(int.from_bytes(self.private_key, byteorder="big")) 25 | public_key = b"\x04" + q[0].to_bytes(32, byteorder="big") + q[1].to_bytes(32, byteorder="big") 26 | hsh = hashlib.sha256(public_key).digest() 27 | 28 | ripemd160hash = hashlib.new("ripemd160") 29 | ripemd160hash.update(hsh) 30 | ripemd160 = ripemd160hash.digest() 31 | 32 | address = b"\x00" + ripemd160 33 | checksum = hashlib.sha256(hashlib.sha256(address).digest()).digest()[:4] 34 | address += checksum 35 | 36 | wif = b"\x80" + self.private_key 37 | checksum = hashlib.sha256(hashlib.sha256(wif).digest()).digest()[:4] 38 | wif += checksum 39 | 40 | address = b58_encode(address) 41 | wif = b58_encode(wif) 42 | return address, wif 43 | 44 | 45 | if __name__ == "__main__": 46 | w = Wallet() 47 | print(w.generate_address()) 48 | -------------------------------------------------------------------------------- /src/dns_seed.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import json 4 | import waitress 5 | from bottle import Bottle, request 6 | 7 | import utils.constants as consts 8 | from utils.logger import logger 9 | 10 | app = Bottle() 11 | 12 | PEER_LIST = [] 13 | 14 | 15 | def validate_peer_list(): 16 | global PEER_LIST 17 | validated_peer_list = [] 18 | for entry in PEER_LIST: 19 | last_time = time.time() 20 | if time.time() - last_time < consts.ENTRY_DURATION: 21 | validated_peer_list.append(entry) 22 | PEER_LIST = validated_peer_list 23 | 24 | 25 | @app.route("/") 26 | def return_peer_list(): 27 | global PEER_LIST 28 | validate_peer_list() 29 | return json.dumps(PEER_LIST) 30 | 31 | 32 | @app.route("/", method="POST") 33 | def update_and_return_peer_list(): 34 | global PEER_LIST 35 | validate_peer_list() 36 | 37 | new_port = request.forms.get("port") 38 | new_ip = request.environ.get("HTTP_X_FORWARDED_FOR") or request.environ.get("REMOTE_ADDR") 39 | ADD_ENTRY = True 40 | 41 | peer_list = [] 42 | for entry in PEER_LIST: 43 | ip = entry["ip"] 44 | port = entry["port"] 45 | if new_port and ip == new_ip and port == new_port: 46 | entry["time"] = time.time() 47 | ADD_ENTRY = False 48 | else: 49 | peer_list.append(entry) 50 | if new_port and ADD_ENTRY: 51 | PEER_LIST.append({"ip": new_ip, "port": new_port, "time": time.time()}) 52 | logger.debug(PEER_LIST) 53 | return json.dumps(peer_list) 54 | 55 | 56 | waitress.serve(app, host="0.0.0.0", threads=4, port=consts.SEED_SERVER_PORT) 57 | -------------------------------------------------------------------------------- /src/utils/secp256k1.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | """ 4 | TODO: 5 | add docstrings 6 | """ 7 | Point = Tuple[int, int] 8 | 9 | # https://en.bitcoin.it/wiki/Secp256k1 10 | P: int = (2 ** 256) - (2 ** 32) - (2 ** 9) - (2 ** 8) - (2 ** 7) - (2 ** 6) - (2 ** 4) - 1 11 | G: Point = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 12 | 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) 13 | B58_char = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' 14 | 15 | 16 | def point_add(p: Point, q: Point) -> Point: 17 | px, py = p 18 | qx, qy = q 19 | 20 | if p == q: 21 | lmbda = pow(2 * py % P, P - 2, P) * (3 * px * px) % P 22 | else: 23 | lmbda = pow(qx - px, P - 2, P) * (qy - py) % P 24 | 25 | rx = (lmbda ** 2 - px - qx) % P 26 | ry = (lmbda * px - lmbda * rx - py) % P 27 | 28 | return rx, ry 29 | 30 | 31 | def point_mul(d: int) -> Point: 32 | p = G 33 | n = p 34 | q = None 35 | 36 | for i in range(256): 37 | if d & (1 << i): 38 | if q is None: 39 | q = n 40 | else: 41 | q = point_add(q, n) 42 | 43 | n = point_add(n, n) 44 | 45 | return q 46 | 47 | 48 | def b58_encode(d: bytes) -> str: 49 | out = "" 50 | p = 0 51 | x = 0 52 | 53 | while d[0] == 0: 54 | out += "1" 55 | d = d[1:] 56 | 57 | for i, v in enumerate(d[::-1]): 58 | x += v * (256 ** i) 59 | 60 | while x > 58 ** (p + 1): 61 | p += 1 62 | 63 | while p >= 0: 64 | a, x = divmod(x, 58 ** p) 65 | out += B58_char[a] 66 | p -= 1 67 | 68 | return out 69 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pychain 2 | channels: 3 | - defaults 4 | dependencies: 5 | - asn1crypto=0.24.0=py37_0 6 | - ca-certificates=2018.03.07=0 7 | - certifi=2018.4.16=py37_0 8 | - cffi=1.11.5=py37h9745a5d_0 9 | - chardet=3.0.4=py37_1 10 | - click=6.7=py37_0 11 | - cryptography=2.2.2=py37h14c3975_0 12 | - flask=1.0.2=py37_1 13 | - idna=2.7=py37_0 14 | - itsdangerous=0.24=py37_1 15 | - jinja2=2.10=py37_0 16 | - libedit=3.1.20170329=h6b74fdf_2 17 | - libffi=3.2.1=hd88cf55_4 18 | - libgcc-ng=7.2.0=hdf63c60_3 19 | - libstdcxx-ng=7.2.0=hdf63c60_3 20 | - markupsafe=1.0=py37h14c3975_1 21 | - ncurses=6.1=hf484d3e_0 22 | - openssl=1.0.2o=h14c3975_1 23 | - pep8=1.7.1=py37_0 24 | - pip=10.0.1=py37_0 25 | - pycparser=2.18=py37_1 26 | - pyopenssl=18.0.0=py37_0 27 | - pysocks=1.6.8=py37_0 28 | - python=3.7.0=hc3d631a_0 29 | - readline=7.0=ha6073c6_4 30 | - requests=2.19.1=py37_0 31 | - setuptools=39.2.0=py37_0 32 | - six=1.11.0=py37_1 33 | - sqlite=3.24.0=h84994c4_0 34 | - tk=8.6.7=hc745277_3 35 | - urllib3=1.23=py37_0 36 | - werkzeug=0.14.1=py37_0 37 | - wheel=0.31.1=py37_0 38 | - xz=5.2.4=h14c3975_4 39 | - zlib=1.2.11=ha838bed_2 40 | - pip: 41 | - appdirs==1.4.3 42 | - astroid==2.0.4 43 | - attrs==18.1.0 44 | - black==18.6b4 45 | - bottle==0.12.13 46 | - ecdsa==0.13 47 | - fastecdsa==1.6.4 48 | - isort==4.3.4 49 | - lazy-object-proxy==1.3.1 50 | - mccabe==0.6.1 51 | - mypy==0.620 52 | - numpy==1.15.1 53 | - pickledb==0.7.1 54 | - pycodestyle==2.3.1 55 | - pyflakes==1.6.0 56 | - pylint==2.1.1 57 | - simplejson==3.16.0 58 | - sqlitedict==1.5.0 59 | - toml==0.9.4 60 | - typed-ast==1.1.0 61 | - waitress==1.1.0 62 | - wrapt==1.10.11 63 | - wsgi-lineprof==0.5.0 64 | prefix: /opt/miniconda3/envs/pychain 65 | 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.class 6 | # IDE Files 7 | .idea/ 8 | .vscode/ 9 | src/*.sql 10 | src/wallet/* 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | -------------------------------------------------------------------------------- /src/transaction_creator.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Any, Dict 4 | 5 | import requests 6 | 7 | from block_creator import first_block_transaction 8 | from core import SingleOutput, Transaction, TxIn, TxOut 9 | from utils import constants as consts 10 | from utils.logger import logger 11 | from utils.utils import dhash 12 | from wallet import Wallet 13 | 14 | 15 | def fetch_peer_list(): 16 | r = requests.get(consts.SEED_SERVER_URL) 17 | peer_list = json.loads(r.text) 18 | return peer_list 19 | 20 | 21 | def get_peer_url(peer: Dict[str, Any]) -> str: 22 | return "http://" + str(peer["ip"]) + ":" + str(peer["port"]) 23 | 24 | 25 | if __name__ == "__main__": 26 | 27 | # The singleOutput for first coinbase transaction in genesis block 28 | so = SingleOutput(txid=dhash(first_block_transaction[0]), vout=0) 29 | 30 | first_transaction = Transaction( 31 | version=1, 32 | locktime=0, 33 | timestamp=3, 34 | is_coinbase=False, 35 | fees=4000000000, 36 | vin={0: TxIn(payout=so, sig="", pub_key=consts.WALLET_PUBLIC)}, 37 | vout={0: TxOut(amount=1000000000, address=consts.WALLET_PUBLIC)}, 38 | ) 39 | 40 | sign_copy_of_tx = copy.deepcopy(first_transaction) 41 | sign_copy_of_tx.vin = {} 42 | w = Wallet() 43 | w.public_key = consts.WALLET_PUBLIC 44 | w.private_key = consts.WALLET_PRIVATE 45 | sig = w.sign(sign_copy_of_tx.to_json()) 46 | first_transaction.vin[0].sig = sig 47 | 48 | peer_list = fetch_peer_list() 49 | print(peer_list) 50 | for peer in peer_list: 51 | try: 52 | print(get_peer_url(peer)) 53 | requests.post(get_peer_url(peer) + "/newtransaction", data={"transaction": first_transaction.to_json()}) 54 | except Exception as e: 55 | logger.debug("TransactionCreator: Could no send transaction") 56 | pass 57 | -------------------------------------------------------------------------------- /src/utils/dataclass_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.__init__ import ChainMap 3 | from dataclasses import asdict, fields 4 | 5 | from .dataclass_json_core import _Encoder, _decode_dataclass 6 | 7 | 8 | class DataClassJson: 9 | def to_json( 10 | self, 11 | *, 12 | skipkeys=False, 13 | ensure_ascii=True, 14 | check_circular=True, 15 | allow_nan=True, 16 | indent=None, 17 | separators=None, 18 | default=None, 19 | sort_keys=True, 20 | **kw 21 | ): 22 | return json.dumps( 23 | asdict(self), 24 | cls=_Encoder, 25 | skipkeys=skipkeys, 26 | ensure_ascii=ensure_ascii, 27 | check_circular=check_circular, 28 | allow_nan=allow_nan, 29 | indent=indent, 30 | separators=separators, 31 | default=default, 32 | sort_keys=sort_keys, 33 | **kw 34 | ) 35 | 36 | @classmethod 37 | def from_json(cls, kvs, *, encoding=None, parse_float=None, parse_int=None, parse_constant=None, infer_missing=False): 38 | init_kwargs = json.loads( 39 | kvs, encoding=encoding, parse_float=parse_float, parse_int=parse_int, parse_constant=parse_constant 40 | ) 41 | 42 | if infer_missing: 43 | init_kwargs = ChainMap(init_kwargs, {field.name: None for field in fields(cls) if field.name not in init_kwargs}) 44 | return _decode_dataclass(cls, init_kwargs) 45 | 46 | @classmethod 47 | def from_json_array(cls, kvss, encoding=None, parse_float=None, parse_int=None, parse_constant=None): 48 | init_kwargs_array = json.loads( 49 | kvss, encoding=encoding, parse_float=parse_float, parse_int=parse_int, parse_constant=parse_constant 50 | ) 51 | return [_decode_dataclass(cls, init_kwargs) for init_kwargs in init_kwargs_array] 52 | -------------------------------------------------------------------------------- /src/views/block.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="explorer") 2 | % from datetime import datetime 3 | 4 |
5 |
6 |

Block Details

7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 47 | 48 |
Block Hash{{ str(block) }}
Block Number{{ block.header.height }}
Timestamp{{ str(datetime.fromtimestamp(block.header.timestamp).strftime("%d-%m-%Y %H:%M:%S")) }} ({{ block.header.timestamp }})
Previous Block Hash{{ block.header.prev_block_hash }}
Merkle Root{{ block.header.merkle_root }}
No. of Transactions{{ len(block.transactions) }}
Transactions 38 | 39 | % for num, transaction in enumerate(block.transactions): 40 | 41 | 42 | 43 | 44 | % end 45 |
{{ num }}{{ transaction.hash() }}
46 |
49 |
50 |
51 |
52 | 53 | % include('footer.html') -------------------------------------------------------------------------------- /src/views/account.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="explorer") 2 | 3 |
4 |
5 |

Account History

6 |
7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 48 | 49 |
Public Key{{ pubkey }}
Balance{{ balance }}
Transactions 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | % for i, d in enumerate(tx_hist): 31 | % import json 32 | % d = json.loads(d) 33 | % if d['amount'] > 0: 34 | 35 | % else: 36 | 37 | % end 38 | 39 | 40 | 41 | 42 | 43 | 44 | % end 45 |
#AmountAddressTimestampTx
{{ i }}{{ d['amount'] }}{{ d['address'] }}{{ d['timestamp'] }}TX
46 |
47 |
50 |
51 |
52 |
53 | 54 | % include('footer.html') -------------------------------------------------------------------------------- /src/views/transaction.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="explorer") 2 | 3 | <% 4 | from datetime import datetime 5 | pub, receivers = tx.summarize() 6 | %> 7 |
8 |
9 |

Transaction Details

10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 52 | 53 |
Transaction Hash{{ tx.hash() }}
Block Hash{{ str(block) }}
Timestamp{{ str(datetime.fromtimestamp(tx.timestamp).strftime("%d-%m-%Y %H:%M:%S")) }} ({{ tx.timestamp }})
Sender Address{{ pub }}
Message{{ tx.message }}
Receivers 37 |
38 | 39 | 40 | 41 | 42 | 43 | % for key, amt in receivers.items(): 44 | 45 | 46 | 47 | 48 | % end 49 |
AmountAddress
{{ amt }}{{ key }}
50 |
51 |
54 |
55 |
56 |
57 | 58 | % include('footer.html') -------------------------------------------------------------------------------- /src/wallet.py: -------------------------------------------------------------------------------- 1 | import json 2 | from fastecdsa import keys, curve, ecdsa 3 | 4 | import utils.constants as consts 5 | from utils.logger import logger 6 | from utils.storage import add_wallet_to_db, get_wallet_from_db 7 | from utils.encode_keys import encode_public_key, decode_public_key 8 | PORT = str(consts.MINER_SERVER_PORT) 9 | 10 | class Wallet: 11 | 12 | private_key: str = None 13 | public_key: str = None 14 | 15 | def __init__(self): 16 | keys = get_wallet_from_db(PORT) 17 | if keys: 18 | self.private_key, self.public_key = keys 19 | logger.info("Wallet: Restoring Existing Wallet") 20 | return 21 | 22 | self.private_key, self.public_key = self.generate_address() 23 | logger.info("Wallet: Creating new Wallet") 24 | logger.info(self) 25 | add_wallet_to_db(PORT, self) 26 | 27 | def __repr__(self): 28 | return f"PubKey:\t{self.public_key}\nPrivKey:\t{self.private_key}" 29 | 30 | def generate_address(self): 31 | priv_key, pub_key_point = keys.gen_keypair(curve.P256) 32 | return priv_key, encode_public_key(pub_key_point) 33 | 34 | def sign(self, transaction: str) -> str: 35 | r, s = ecdsa.sign(transaction, self.private_key, curve=curve.P256) 36 | return json.dumps((r, s)) 37 | 38 | @staticmethod 39 | def verify(transaction: str, signature: str, public_key: str) -> bool: 40 | r, s = json.loads(signature) 41 | public_key = decode_public_key(public_key) 42 | return ecdsa.verify((r, s), transaction, public_key, curve=curve.P256) 43 | 44 | 45 | if __name__ == "__main__": 46 | w = Wallet() 47 | print(w) 48 | 49 | # print(w.public_key) 50 | # print("-----------------------------------------------------------") 51 | # print(w.private_key) 52 | 53 | # message = "VJTI" 54 | # sig = w.sign(message) 55 | # print(sig) 56 | 57 | message = "VJTI" 58 | sig = "[89577411930164173462307433514969836725411494465308869182568430542388619107505, 485183603865849592066733357268024106189235193197266488152700409427460402051]" 59 | pubKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyDSDFUjXKp/s+37wjME5thvgCnMJ3XcOnjXxJ6K4IdrF5x7MIbF+nkbZYLMKk1TUK/ZIX1b3F6F320q5EHLUmw==" 60 | result = Wallet.verify(message, sig, pubKey) 61 | print(result) 62 | -------------------------------------------------------------------------------- /src/utils/storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import TYPE_CHECKING 3 | 4 | from sqlitedict import SqliteDict 5 | 6 | from .constants import BLOCK_DB_LOC, CHAIN_DB_LOC, WALLET_DB_LOC, NEW_BLOCKCHAIN 7 | from .utils import dhash 8 | from .encode_keys import encode_public_key 9 | 10 | from fastecdsa.keys import export_key, import_key 11 | from fastecdsa.curve import secp256k1, P256 12 | 13 | from json import loads, dumps 14 | 15 | if TYPE_CHECKING: 16 | import sys 17 | 18 | sys.path.append(os.path.split(sys.path[0])[0]) 19 | 20 | from src.core import Block # noqa 21 | from src.wallet import Wallet # noqa 22 | 23 | WALLET_DB = None 24 | 25 | if NEW_BLOCKCHAIN: 26 | try: 27 | os.remove(BLOCK_DB_LOC) 28 | except OSError: 29 | pass 30 | 31 | 32 | # WALLET FUNCTIONS 33 | def get_wallet_from_db(port: str) -> str: 34 | try: 35 | location = WALLET_DB_LOC + str(port) + ".key" 36 | priv_key, pub_key_point = import_key(location) 37 | return priv_key, encode_public_key(pub_key_point) 38 | except Exception as e: 39 | return None 40 | 41 | 42 | def add_wallet_to_db(port: str, wallet: "Wallet"): 43 | location = WALLET_DB_LOC + str(port) 44 | export_key(wallet.private_key, curve=P256, filepath=location + ".key") 45 | with open(location + ".pub", "w") as file: 46 | file.write(wallet.public_key) 47 | 48 | 49 | # BLOCK FUNCTIONS 50 | def get_block_from_db(header_hash: str) -> str: 51 | with SqliteDict(BLOCK_DB_LOC, autocommit=False) as db: 52 | return db.get(header_hash, None) 53 | 54 | 55 | def add_block_to_db(block: "Block"): 56 | with SqliteDict(BLOCK_DB_LOC, autocommit=False) as db: 57 | db[dhash(block.header)] = block.to_json() 58 | db.commit(blocking=False) 59 | 60 | 61 | def check_block_in_db(header_hash: str) -> bool: 62 | with SqliteDict(BLOCK_DB_LOC, autocommit=False) as db: 63 | if db.get(header_hash, None): 64 | return True 65 | return False 66 | 67 | 68 | def remove_block_from_db(header_hash: str): 69 | with SqliteDict(BLOCK_DB_LOC, autocommit=False) as db: 70 | del db[header_hash] 71 | db.commit() 72 | 73 | 74 | # Active Chain functions 75 | def write_header_list_to_db(header_list: list): 76 | with open(CHAIN_DB_LOC, "w") as file: 77 | headers = list(map(dhash, header_list)) 78 | file.write(dumps(headers)) 79 | 80 | 81 | def read_header_list_from_db(): 82 | with open(CHAIN_DB_LOC, "r") as file: 83 | data = file.read() 84 | if data: 85 | return loads(data) 86 | return None 87 | -------------------------------------------------------------------------------- /src/utils/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import hashlib 3 | import json 4 | import zlib as zl 5 | from base64 import b85decode, b85encode 6 | from functools import wraps 7 | from typing import TYPE_CHECKING, List, Union 8 | 9 | from . import constants as consts 10 | 11 | if TYPE_CHECKING: 12 | import os 13 | import sys 14 | 15 | sys.path.append(os.path.split(sys.path[0])[0]) 16 | from src.core import Transaction, BlockHeader # noqa 17 | 18 | 19 | def get_time_difference_from_now_secs(timestamp: int) -> int: 20 | """Get time diference from current time in seconds 21 | 22 | Arguments: 23 | timestamp {int} -- Time from which difference is calculated 24 | 25 | Returns: 26 | int -- Time difference in seconds 27 | """ 28 | 29 | now = datetime.datetime.now() 30 | mtime = datetime.datetime.fromtimestamp(timestamp) 31 | difference = mtime - now 32 | return int(difference.total_seconds()) 33 | 34 | 35 | def merkle_hash(transactions: List["Transaction"]) -> str: 36 | """ Computes and returns the merkle tree root for a list of transactions """ 37 | if transactions is None or len(transactions) == 0: 38 | return "F" * consts.HASH_LENGTH_HEX 39 | 40 | transactions_hash = list(map(dhash, transactions)) 41 | 42 | def recursive_merkle_hash(t: List[str]) -> str: 43 | if len(t) == 1: 44 | return t[0] 45 | if len(t) % 2 != 0: 46 | t = t + [t[-1]] 47 | t_child = [] 48 | for i in range(0, len(t), 2): 49 | new_hash = dhash(t[i] + t[i + 1]) 50 | t_child.append(new_hash) 51 | return recursive_merkle_hash(t_child) 52 | 53 | return recursive_merkle_hash(transactions_hash) 54 | 55 | 56 | def dhash(s: Union[str, "Transaction", "BlockHeader"]) -> str: 57 | """ Double sha256 hash """ 58 | if not isinstance(s, str): 59 | s = str(s) 60 | s = s.encode() 61 | return hashlib.sha256(hashlib.sha256(s).digest()).hexdigest() 62 | 63 | 64 | def lock(lock): 65 | def decorator(f): 66 | @wraps(f) 67 | def call(*args, **kwargs): 68 | with lock: 69 | return f(*args, **kwargs) 70 | 71 | return call 72 | 73 | return decorator 74 | 75 | 76 | def compress(payload: str) -> bytes: 77 | return b85encode(zl.compress(payload.encode(), zl.Z_BEST_COMPRESSION)) 78 | 79 | 80 | def decompress(payload: bytes) -> str: 81 | return zl.decompress(b85decode(payload)).decode() 82 | 83 | 84 | def generate_tx_hist(amount, address, timestamp, bhash, thash, message): 85 | data = {} 86 | data["amount"] = amount 87 | data["address"] = address 88 | data["timestamp"] = timestamp 89 | data["bhash"] = bhash 90 | data["thash"] = thash 91 | data["message"] = message 92 | return json.dumps(data) 93 | -------------------------------------------------------------------------------- /src/views/chains.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="chains") 2 | 3 | 4 | 5 | 6 | 25 | 26 |
27 | 33 |
34 | 35 | 81 | 82 | % include('footer.html') 83 | -------------------------------------------------------------------------------- /src/views/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VJTI Chain 6 | 7 | 8 | 9 | 11 | 14 | 17 | 20 | 21 | 60 | 61 | 62 | 63 | 84 | ​ -------------------------------------------------------------------------------- /src/views/index.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="home") 2 | 3 |
4 | 5 | % if message!= "": 6 | 12 | % end 13 | 14 |
15 |
16 |

Faucet

17 |
18 |
19 | 23 |
24 |
25 | 26 | 28 | 6 characters, case sensitive. It was emailed to you. 29 |
30 |
31 | 32 | 33 | Copy your Public Key from your profile in the Wallet App. 34 |
35 | 36 |
37 |
38 |
39 | 72 |
73 | 74 | % include('footer.html') -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VJTI Chain 2 | A complete implementation of a Proof of Authority (POA) Blockchain in python 3.7+ 3 | 4 | ## Simplifications 5 | - Storage using pickledb in flat files 6 | - Communication between peers using http api calls 7 | - Can only send tokens to a single public key 8 | - Serialization completely done via json 9 | - No scripting language 10 | - All nodes assumed to be honest and non malicious 11 | - Peer discovery through a central server 12 | - Every node is a full node with a wallet, light nodes would be implemented as Android Apps. 13 | 14 | ## Installing and running 15 | Use `conda` to create an env using the `environment.yml` file and run `src/fullnode.py` 16 | 17 | #### Installing Dependencies 18 | ```bash 19 | sudo apt-get install python-dev libgmp3-dev wget gcc #for fastecdsa 20 | ``` 21 | 22 | #### Installing Miniconda 23 | ```bash 24 | wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh 25 | bash Miniconda3-latest-Linux-x86_64.sh # Follow the instructions and ensure that conda is added to shell path. 26 | ``` 27 | 28 | #### Creating conda environment 29 | ```bash 30 | # Inside the Repo directory 31 | cd somechain/ 32 | conda env create -f=./environment.yml 33 | # If creation fails, you might need to delete the environment before retrying 34 | # conda env remove -n pychain 35 | ``` 36 | 37 | #### Running 38 | ```bash 39 | cd src/ 40 | source activate pychain 41 | # You will need to run 2 processes. We suggest using a terminal multiplexer like tmux or screen. 42 | # tmux 43 | python dns_seed.py # Run the central dns server for peer discovery 44 | python fullnode.py -p 9000 -n -q # Run the full node on port(-p) 9000, (-n) new blockchain from genesis i.e. no restore and in quiet mode(-q) 45 | # To terminate press ctrl+C twice. 46 | ``` 47 | 48 | 49 | #### Add DDOS ban 50 | ```bash 51 | sudo apt install fail2ban 52 | sudo cp ddoskill.conf /etc/fail2ban/filter.d/ 53 | # Change Log file location in jail.local before copying 54 | sudo cp jail.local /etc/fail2ban/ 55 | sudo service fail2ban start # or restart 56 | sudo fail2ban-client reload # or start 57 | # To check ban status 58 | sudo fail2ban-client status ddoskill 59 | ``` 60 | 61 | #### Using Apache (For SSL) 62 | 63 | Follow [this](https://www.digitalocean.com/community/tutorials/how-to-secure-apache-with-let-s-encrypt-on-ubuntu-18-04) for latest methods to install apache with Let's Encrypt certbot for SSL. 64 | ```bash 65 | sudo add-apt-repository ppa:certbot/certbot 66 | sudo apt install python-certbot-apache 67 | sudo apt-get install apache2 68 | ``` 69 | 70 | Enable Mods for proxy 71 | ``` 72 | sudo a2enmod proxy 73 | sudo a2enmod proxy_http 74 | sudo a2enmod proxy_balancer 75 | sudo a2enmod lbmethod_byrequests 76 | sudo a2enmod rewrite 77 | ``` 78 | 79 | You will need to modify your Apache config. 80 | ``` 81 | # sudo vim /etc/apache2/sites-available/000-default.conf 82 | # You Will need to write your own config file. Here a snippet of what our file looks like 83 | 84 | 85 | ServerName # example.com 86 | ServerAlias # chain.example.com 87 | ProxyPreserveHost On 88 | ProxyPass / http://0.0.0.0:9000/ 89 | ProxyPassReverse / http://0.0.0.0:9000/ 90 | 91 | 92 | # Here we redirect all requests that come to example.com at port 80 to 93 | # our chain that is running locally at port 9000. 94 | ``` 95 | 96 | Now we need to enable the certbot 97 | ``` 98 | sudo ufw allow 'Apache Full' 99 | sudo certbot --apache -d -d 100 | # Folow the stes as asked by the bot 101 | sudo systemctl restart apache2 102 | ``` 103 | -------------------------------------------------------------------------------- /src/utils/constants.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import sys 4 | 5 | # LOGGING CONSTANTS 6 | LOG_DIRECTORY = "log/" 7 | DATE_FORMAT = "%b %d %H:%M:%S" 8 | LOG_LEVEL = logging.DEBUG 9 | 10 | # DNS SEED CONSTANTS 11 | ENTRY_DURATION = 60 * 10 # duration in seconds 12 | SEED_SERVER_URL = "http://localhost:8080" 13 | SEED_SERVER_PORT = 8080 14 | 15 | # MINER CONSTANTS 16 | MINER_SERVER_PORT = 9000 17 | MINER_VERSION = 1 18 | MINING_INTERVAL_THRESHOLD = 5 # Seconds 19 | MINING_TRANSACTION_THRESHOLD = 10 # No. of Transactions 20 | 21 | # BLOCKCHAIN CONSTANTS 22 | HASH_LENGTH_HEX = 64 # 256 bit string is 64 hexa_dec string 23 | 24 | PUBLIC_KEY_LENGTH = 124 # Length of Armoured Public Key 25 | 26 | MAX_MESSAGE_SIZE = 128 # Maximum Message Length for each Transaction 27 | 28 | FORK_CHAIN_HEIGHT = 7 # Keep only chains that are within this height of the active chain 29 | 30 | MAX_BLOCK_SIZE_KB = 4096 31 | MAX_COINS_POSSIBLE = 10000000 * 10 32 | 33 | INITIAL_BLOCK_REWARD = 5 * 100 34 | REWARD_UPDATE_INTERVAL = 20_000 35 | 36 | # A block cannot have timestamp greater than this time in the future 37 | BLOCK_MAX_TIME_FUTURE_SECS = 2 * 60 * 60 38 | 39 | INITIAL_BLOCK_DIFFICULTY = 1 40 | 41 | BLOCK_DIFFICULTY_UPDATE_INTERVAL = 5 # number of blocks 42 | AVERAGE_BLOCK_MINE_INTERVAL = 2 * 60 # seconds 43 | MAXIMUM_TARGET_DIFFICULTY = "0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" 44 | 45 | # Cheat Code 46 | BLOCK_MINING_SPEEDUP = 1 47 | 48 | # Max History 49 | MAX_TRANSACTION_HISTORY_TO_KEEP = 2048 50 | 51 | # Define Values from arguments passed 52 | parser = argparse.ArgumentParser() 53 | 54 | parser.add_argument("--version", help="Print Implementation Version", action="store_true") 55 | parser.add_argument("-p", "--port", type=int, help="Port on which the fullnode should run", default=MINER_SERVER_PORT) 56 | parser.add_argument("-s", "--seed-server", type=str, help="Url on which the DNS seed server is running", default=SEED_SERVER_URL) 57 | parser.add_argument("-nm", "--no-mining", help="Do not Mine", action="store_true") 58 | parser.add_argument("-n", "--new-blockchain", help="Start a new Blockchain from Genesis Block", action="store_true") 59 | group = parser.add_mutually_exclusive_group() 60 | group.add_argument("-v", "--verbose", action="store_true") 61 | group.add_argument("-q", "--quiet", action="store_true") 62 | args = parser.parse_args() 63 | 64 | # Print Somechain Version 65 | if args.version: 66 | print("## Somchain Version: " + str(MINER_VERSION) + " ##") 67 | sys.exit(0) 68 | 69 | # Set Logging Level 70 | if args.quiet: 71 | LOG_LEVEL = logging.INFO 72 | elif args.verbose: 73 | LOG_LEVEL = logging.DEBUG 74 | 75 | # Set Server Port 76 | MINER_SERVER_PORT = args.port 77 | 78 | # Set Seed Server URL 79 | SEED_SERVER_URL = args.seed_server 80 | 81 | # Set if create new blockchain 82 | if args.new_blockchain: 83 | NEW_BLOCKCHAIN = True 84 | else: 85 | NEW_BLOCKCHAIN = False 86 | 87 | # Set if to mine of not 88 | if args.no_mining: 89 | NO_MINING = True 90 | else: 91 | NO_MINING = False 92 | 93 | 94 | # Coinbase Maturity 95 | COINBASE_MATURITY = 0 96 | 97 | # Genesis Block Sign 98 | GENESIS_BLOCK_SIGNATURE = "4093f844282309feb788feb2d3a81946cbc70478360f0d0fe581e1425027feaa9992553797ce1aa005eb0f23824edef7582997a289e45696143bc5f55dd55a47" 99 | 100 | # DB CONSTANTS 101 | BLOCK_DB_LOC = "db/" + str(MINER_SERVER_PORT) + "block.sqlite" 102 | CHAIN_DB_LOC = "db/" + str(MINER_SERVER_PORT) + "chain.json" 103 | 104 | # WALLET CONSTANTS 105 | WALLET_DB_LOC = "wallet/" 106 | 107 | # AUTHORITY RULES 108 | AUTHORITY_RULES_LOC = "authority_rules.json" 109 | -------------------------------------------------------------------------------- /src/views/about.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="about") 2 | 3 |
4 |

Welcome to VJTI Chain!

5 |

Blockchain for Global Good

6 |
7 | 8 |
9 |
10 |
11 |
12 |

13 | 17 |

18 |
19 | 20 |
21 |
22 |
    23 |
  • A complete Proof of Authority blockchain written in Python from scratch. (only using 24 | external networking and cryptographic libraries)
  • 25 |
  • Supports token transfers, backed by INR.
  • 26 |
  • Access via API, to help students build their own applications on top of the blockchain.
  • 27 |
  • Open source and easily modifiable. You can find the source code here.
  • 28 | 29 |
30 |

31 | Also you can download the Android Wallet App from here. 32 |

33 |
34 |
35 |
36 |
37 |
38 |

39 | 43 |

44 |
45 |
46 |
47 |

Canteen

48 |

Xerox Center

49 |
50 |
51 |
52 |
53 |
54 |

55 | 59 |

60 |
61 |
62 |
63 |

64 | Weekly Challenges 65 |

66 |

67 | Class Assignments 68 |

69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 | % include('footer.html') -------------------------------------------------------------------------------- /src/views/wallet.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="wallet") 2 | 3 | 6 | 7 |
8 |

Send Coins

9 |
10 |
11 |

My PublicKey:

{{ pubkey }} 12 |
13 |
14 |

Balance: 15 | 0 16 | 17 | vjcoins 18 | 19 |

20 |
21 |
22 | 23 | 24 | % if message!= "": 25 | 31 | % end 32 | 33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 | 46 | 101 | 102 | % include('footer.html') -------------------------------------------------------------------------------- /src/authority.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import time 3 | from datetime import datetime 4 | import json 5 | from multiprocessing import Process 6 | from sys import getsizeof 7 | from typing import List, Optional, Set, Tuple 8 | 9 | import requests 10 | 11 | import utils.constants as consts 12 | from core import Block, BlockHeader, Chain, Transaction, SingleOutput 13 | from utils.logger import logger 14 | from utils.utils import compress, dhash, merkle_hash, get_time_difference_from_now_secs 15 | from wallet import Wallet 16 | 17 | from authority_rules import authority_rules 18 | 19 | 20 | def is_my_turn(wallet): 21 | timestamp = datetime.now() 22 | seconds_since_midnight = (timestamp - timestamp.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() 23 | for authority in authority_rules["authorities"]: 24 | if seconds_since_midnight <= authority["to"] and seconds_since_midnight >= authority["from"]: 25 | if wallet.public_key == authority["pubkey"]: 26 | return True 27 | return False 28 | 29 | 30 | class Authority: 31 | def __init__(self): 32 | self.p: Optional[Process] = None 33 | 34 | def is_mining(self): 35 | if self.p: 36 | if self.p.is_alive(): 37 | return True 38 | else: 39 | self.p = None 40 | return False 41 | 42 | def start_mining(self, mempool: Set[Transaction], chain: Chain, wallet: Wallet): 43 | if not self.is_mining(): 44 | if is_my_turn(wallet): 45 | if len(mempool) > consts.MINING_TRANSACTION_THRESHOLD or ( 46 | len(mempool) > 0 47 | and abs(get_time_difference_from_now_secs(chain.header_list[-1].timestamp)) > consts.MINING_INTERVAL_THRESHOLD 48 | ): 49 | local_utxo = copy.deepcopy(chain.utxo) 50 | mempool_copy = copy.deepcopy(mempool) 51 | # Validating each transaction in block 52 | for t in mempool_copy: 53 | # Remove the spent outputs 54 | for tinput in t.vin: 55 | so = t.vin[tinput].payout 56 | if so: 57 | if local_utxo.get(so)[0] is not None: 58 | local_utxo.remove(so) 59 | else: 60 | mempool.remove(t) 61 | else: 62 | mempool.remove(t) 63 | self.p = Process(target=self.__mine, args=(mempool, chain, wallet)) 64 | self.p.start() 65 | logger.debug("Miner: Started mining") 66 | 67 | def stop_mining(self): 68 | if self.is_mining(): 69 | # logger.debug("Miner: Called Stop Mining") 70 | self.p.terminate() 71 | self.p = None 72 | 73 | def __calculate_transactions(self, transactions: List[Transaction]) -> List[Transaction]: 74 | i = 0 75 | size = 0 76 | mlist = [] 77 | while i < len(transactions) and size <= consts.MAX_BLOCK_SIZE_KB: 78 | t = transactions[i] 79 | mlist.append(t) 80 | size += getsizeof(t.to_json()) 81 | i += 1 82 | return mlist 83 | 84 | def __mine(self, mempool: Set[Transaction], chain: Chain, wallet: Wallet) -> Block: 85 | c_pool = list(copy.deepcopy(mempool)) 86 | mlist = self.__calculate_transactions(c_pool) 87 | logger.debug(len(mlist)) 88 | 89 | block_header = BlockHeader( 90 | version=consts.MINER_VERSION, 91 | height=chain.length, 92 | prev_block_hash=dhash(chain.header_list[-1]), 93 | merkle_root=merkle_hash(mlist), 94 | timestamp=int(time.time()), 95 | signature="", 96 | ) 97 | 98 | sign = wallet.sign(dhash(block_header)) 99 | block_header.signature = sign 100 | block = Block(header=block_header, transactions=mlist) 101 | requests.post("http://0.0.0.0:" + str(consts.MINER_SERVER_PORT) + "/newblock", data=compress(block.to_json())) 102 | logger.info(f"Miner: Mined Block with {len(mlist)} transactions.") 103 | return 104 | -------------------------------------------------------------------------------- /src/miner.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import sys 3 | import time 4 | from multiprocessing import Process 5 | from operator import attrgetter 6 | from sys import getsizeof 7 | from typing import List, Optional, Set, Tuple 8 | 9 | import requests 10 | 11 | import utils.constants as consts 12 | from core import Block, BlockHeader, Chain, Transaction, TxIn, TxOut 13 | from utils.logger import logger 14 | from utils.utils import compress, dhash, merkle_hash 15 | 16 | 17 | class Miner: 18 | def __init__(self): 19 | self.p: Optional[Process] = None 20 | 21 | def is_mining(self): 22 | if self.p: 23 | if self.p.is_alive(): 24 | return True 25 | else: 26 | self.p = None 27 | return False 28 | 29 | def start_mining(self, mempool: Set[Transaction], chain: Chain, payout_addr: str): 30 | if not self.is_mining(): 31 | self.p = Process(target=self.__mine, args=(mempool, chain, payout_addr)) 32 | self.p.start() 33 | # logger.debug("Started mining") 34 | 35 | def stop_mining(self): 36 | if self.is_mining(): 37 | # logger.debug("Miner: Called Stop Mining") 38 | self.p.terminate() 39 | self.p = None 40 | 41 | def calculate_transaction_fees_and_size(self, transactions: List[Transaction]) -> Tuple[int, int]: 42 | transactions.sort(key=attrgetter("fees"), reverse=True) 43 | size = 0 44 | fees = 0 45 | for t in transactions: 46 | size += sys.getsizeof(t.to_json()) 47 | fees += t.fees 48 | return fees, size 49 | 50 | def __calculate_best_transactions(self, transactions: List[Transaction]) -> Tuple[List[Transaction], int]: 51 | """Returns the best transactions to be mined which don't exceed the max block size 52 | 53 | Arguments: 54 | transactions {List[Transaction]} -- The transactions to be mined 55 | 56 | Returns: 57 | List[Transaction] -- the transactions which give the best fees 58 | int -- The fees in scoins 59 | """ 60 | transactions.sort(key=attrgetter("fees"), reverse=True) 61 | size = 0 62 | fees = 0 63 | mlist = [] 64 | for t in transactions: 65 | if size < consts.MAX_BLOCK_SIZE_KB: 66 | mlist.append(t) 67 | size += getsizeof(t.to_json()) 68 | fees += t.fees 69 | else: 70 | break 71 | return mlist, fees 72 | 73 | def __mine(self, mempool: Set[Transaction], chain: Chain, payout_addr: str) -> Block: 74 | c_pool = list(copy.deepcopy(mempool)) 75 | mlist, fees = self.__calculate_best_transactions(c_pool) 76 | # logger.debug(f"Miner: Will mine {len(mlist)} transactions and get {fees} scoins in fees") 77 | coinbase_tx_in = {0: TxIn(payout=None, sig="Receiving some Money", pub_key="Does it matter?")} 78 | coinbase_tx_out = { 79 | 0: TxOut(amount=chain.current_block_reward(), address=payout_addr), 80 | 1: TxOut(amount=fees, address=payout_addr), 81 | } 82 | coinbase_tx = Transaction( 83 | is_coinbase=True, 84 | version=consts.MINER_VERSION, 85 | fees=0, 86 | timestamp=int(time.time()), 87 | locktime=-1, 88 | vin=coinbase_tx_in, 89 | vout=coinbase_tx_out, 90 | ) 91 | mlist.insert(0, coinbase_tx) 92 | block_header = BlockHeader( 93 | version=consts.MINER_VERSION, 94 | height=chain.length, 95 | prev_block_hash=dhash(chain.header_list[-1]), 96 | merkle_root=merkle_hash(mlist), 97 | timestamp=int(time.time()), 98 | target_difficulty=chain.target_difficulty, 99 | nonce=0, 100 | ) 101 | DONE = False 102 | for n in range(2 ** 64): 103 | block_header.nonce = n 104 | bhash = dhash(block_header) 105 | if chain.is_proper_difficulty(bhash): 106 | block = Block(header=block_header, transactions=mlist) 107 | requests.post("http://0.0.0.0:" + str(consts.MINER_SERVER_PORT) + "/newblock", data=compress(block.to_json())) 108 | logger.info( 109 | f"Miner: Mined Block with {len(mlist)} transactions, Got {fees} in fees and {chain.current_block_reward()} as reward" 110 | ) 111 | DONE = True 112 | break 113 | if not DONE: 114 | logger.error("Miner: Exhausted all 2 ** 64 values without finding proper hash") 115 | -------------------------------------------------------------------------------- /src/utils/dataclass_json_core.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from dataclasses import fields, is_dataclass 4 | from typing import Collection, Optional 5 | 6 | 7 | def _get_type_origin(type_): 8 | """Some spaghetti logic to accommodate differences between 3.6 and 3.7 in 9 | the typing api""" 10 | try: 11 | origin = type_.__origin__ 12 | except AttributeError: 13 | if sys.version_info.minor == 6: 14 | try: 15 | origin = type_.__extra__ 16 | except AttributeError: 17 | origin = type_ 18 | else: 19 | origin = type_ if origin is None else origin 20 | else: 21 | origin = type_ 22 | return origin 23 | 24 | 25 | def _get_type_cons(type_): 26 | """More spaghetti logic for 3.6 vs. 3.7""" 27 | if sys.version_info.minor == 6: 28 | try: 29 | cons = type_.__extra__ 30 | except AttributeError: 31 | try: 32 | cons = type.__origin__ 33 | except AttributeError: 34 | cons = type_ 35 | else: 36 | cons = type_ if cons is None else cons 37 | else: 38 | try: 39 | cons = type.__origin__ if cons is None else cons 40 | except AttributeError: 41 | cons = type_ 42 | else: 43 | cons = type_.__origin__ 44 | return cons 45 | 46 | 47 | class _Encoder(json.JSONEncoder): 48 | def default(self, o): 49 | if _isinstance_safe(o, Collection): 50 | return list(o) 51 | return json.JSONEncoder.default(self, o) 52 | 53 | 54 | def _decode_dataclass(cls, kvs): 55 | init_kwargs = {} 56 | for field in fields(cls): 57 | field_value = kvs[field.name] 58 | if is_dataclass(field.type): 59 | init_kwargs[field.name] = _decode_dataclass(field.type, field_value) 60 | elif _is_supported_generic(field.type) and field.type != str: 61 | init_kwargs[field.name] = _decode_generic(field.type, field_value) 62 | else: 63 | init_kwargs[field.name] = field_value 64 | return cls(**init_kwargs) 65 | 66 | 67 | def _is_supported_generic(type_): 68 | try: 69 | # __origin__ exists in 3.7 on user defined generics 70 | is_collection = _issubclass_safe(type_.__origin__, Collection) 71 | except AttributeError: 72 | return False 73 | is_optional = _issubclass_safe(type_, Optional) or _hasargs(type_, type(None)) 74 | return is_collection or is_optional 75 | 76 | 77 | def _decode_generic(type_, value): 78 | if value is None: 79 | res = value 80 | elif _issubclass_safe(_get_type_origin(type_), Collection): 81 | # this is a tricky situation where we need to check both the annotated 82 | # type info (which is usually a type from `typing`) and check the 83 | # value's type directly using `type()`. 84 | # 85 | # if the type_arg is a generic we can use the annotated type, but if the 86 | # type_arg is a typevar we need to extract the reified type information 87 | # hence the check of `is_dataclass(value)` 88 | type_arg = type_.__args__[0] 89 | if is_dataclass(type_arg) or is_dataclass(value): 90 | xs = (_decode_dataclass(type_arg, v) for v in value) 91 | elif _is_supported_generic(type_arg): 92 | xs = (_decode_generic(type_arg, v) for v in value) 93 | else: 94 | xs = value 95 | # get the constructor if using corresponding generic type in `typing` 96 | # otherwise fallback on the type returned by 97 | try: 98 | res = _get_type_cons(type_)(xs) 99 | except TypeError: 100 | res = type_(xs) 101 | else: # Optional 102 | type_arg = type_.__args__[0] 103 | if is_dataclass(type_arg) or is_dataclass(value): 104 | res = _decode_dataclass(type_arg, value) 105 | elif _is_supported_generic(type_arg): 106 | res = _decode_generic(type_arg, value) 107 | else: 108 | res = value 109 | return res 110 | 111 | 112 | def _issubclass_safe(cls, classinfo): 113 | try: 114 | result = issubclass(cls, classinfo) 115 | except Exception: 116 | return False 117 | else: 118 | return result 119 | 120 | 121 | def _isinstance_safe(o, t): 122 | try: 123 | result = isinstance(o, t) 124 | except Exception: 125 | return False 126 | else: 127 | return result 128 | 129 | 130 | def _hasargs(type_, *args): 131 | try: 132 | res = all(arg in type_.__args__ for arg in args) 133 | except AttributeError: 134 | return False 135 | else: 136 | return res 137 | -------------------------------------------------------------------------------- /src/block_creator.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | from typing import Any, Dict, List 4 | 5 | import requests 6 | from flask import Flask, jsonify, request 7 | 8 | from core import (Block, BlockHeader, Chain, SingleOutput, Transaction, TxIn, 9 | TxOut, genesis_block, genesis_block_header, 10 | genesis_block_transaction) 11 | from utils import constants as consts 12 | from utils.logger import logger 13 | from utils.storage import get_block_from_db 14 | from utils.utils import dhash, merkle_hash 15 | 16 | app = Flask(__name__) 17 | 18 | PEER_LIST = [] 19 | 20 | BLOCK_DB = None 21 | 22 | ACTIVE_CHAIN = Chain() 23 | 24 | 25 | def fetch_peer_list(): 26 | r = requests.post(consts.SEED_SERVER_URL, data={"port": consts.MINER_SERVER_PORT}) 27 | peer_list = json.loads(r.text) 28 | return peer_list 29 | 30 | 31 | def get_peer_url(peer: Dict[str, Any]) -> str: 32 | return "http://" + str(peer["ip"]) + ":" + str(peer["port"]) 33 | 34 | 35 | def greet_peer(peer: Dict[str, Any]) -> List: 36 | url = get_peer_url(peer) 37 | r = requests.get(url) 38 | return json.loads(r.text) 39 | 40 | 41 | def receive_block_from_peer(peer: Dict[str, Any], header_hash) -> Block: 42 | r = requests.post(get_peer_url(peer), data={"header_hash": header_hash}) 43 | return Block.from_json(r.text) 44 | 45 | 46 | def sync(peer_list): 47 | max_peer = max(peer_list, key=lambda k: k["blockheight"]) 48 | r = requests.post(get_peer_url(max_peer) + "/getblockhashes/", data={"myheight": len(ACTIVE_CHAIN)}) 49 | hash_list = json.loads(r.text) 50 | for hhash in hash_list: 51 | peer_url = get_peer_url(random.choice(peer_list)) + "/getblock/" 52 | r = requests.post(peer_url, data={"headerhash": hhash}) 53 | block = Block.from_json(r.text) 54 | if not ACTIVE_CHAIN.add_block(block): 55 | raise Exception("WTF") 56 | 57 | 58 | @app.route("/") 59 | def hello(): 60 | data = {"version": consts.MINER_VERSION, "blockheight": ACTIVE_CHAIN.length} 61 | return jsonify(data) 62 | 63 | 64 | @app.route("/getblock", methods=["POST"]) 65 | def getblock(): 66 | hhash = request.form.get("headerhash") 67 | if hhash: 68 | return get_block_from_db(hhash) 69 | return "Hash hi nahi bheja LOL" 70 | 71 | 72 | @app.route("/getblockhashes", methods=["POST"]) 73 | def send_block_hashes(): 74 | peer_height = int(request.form.get("myheight")) 75 | hash_list = [] 76 | for i in range(peer_height, ACTIVE_CHAIN.length): 77 | hash_list.append(dhash(ACTIVE_CHAIN.header_list[i])) 78 | logger.debug(peer_height) 79 | return jsonify(hash_list) 80 | 81 | 82 | # The singleOutput for first coinbase transaction in genesis block 83 | so = SingleOutput(txid=dhash(genesis_block_transaction[0]), vout=0) 84 | 85 | first_block_transactions = [ 86 | Transaction( 87 | version=1, 88 | locktime=0, 89 | timestamp=2, 90 | is_coinbase=True, 91 | fees=0, 92 | vin={0: TxIn(payout=None, sig="", pub_key=consts.WALLET_PUBLIC)}, 93 | vout={ 94 | 0: TxOut(amount=5000000000, address=consts.WALLET_PUBLIC), 95 | 1: TxOut(amount=4000000000, address=consts.WALLET_PUBLIC), 96 | }, 97 | ), 98 | Transaction( 99 | version=1, 100 | locktime=0, 101 | timestamp=3, 102 | is_coinbase=False, 103 | fees=4000000000, 104 | vin={0: TxIn(payout=so, sig="", pub_key=consts.WALLET_PUBLIC)}, 105 | vout={0: TxOut(amount=1000000000, address=consts.WALLET_PUBLIC)}, 106 | ), 107 | ] 108 | 109 | 110 | for tx in first_block_transactions: 111 | tx.sign() 112 | 113 | first_block_header = BlockHeader( 114 | version=1, 115 | prev_block_hash=dhash(genesis_block_header), 116 | height=1, 117 | merkle_root=merkle_hash(first_block_transactions), 118 | timestamp=1231006505, 119 | target_difficulty=0, 120 | nonce=2083236893, 121 | ) 122 | first_block = Block(header=first_block_header, transactions=first_block_transactions) 123 | 124 | if __name__ == "__main__": 125 | 126 | result = ACTIVE_CHAIN.add_block(genesis_block) 127 | logger.debug(result) 128 | 129 | logger.debug(ACTIVE_CHAIN.utxo) 130 | 131 | result = ACTIVE_CHAIN.add_block(first_block) 132 | logger.debug(result) 133 | logger.debug(ACTIVE_CHAIN.utxo) 134 | 135 | # # ORDER 136 | # Get list of peers ✓ 137 | # Contact peers and get current state of blockchain ✓ 138 | # Sync upto the current blockchain ✓ 139 | # Start the flask server and listen for future blocks and transactions. 140 | # Start a thread to handle the new block/transaction 141 | 142 | fetch_peer_list() 143 | 144 | app.run(host="0.0.0.0", port=consts.MINER_SERVER_PORT, threaded=True, debug=True) 145 | -------------------------------------------------------------------------------- /src/wallet.java: -------------------------------------------------------------------------------- 1 | import java.math.BigInteger; 2 | import java.security.*; 3 | import java.io.*; 4 | import java.nio.charset.*; 5 | import java.security.spec.*; 6 | import java.util.*; 7 | 8 | public class wallet { 9 | 10 | private static String getKey(String filename) throws IOException { 11 | // Read key from file 12 | String strKeyPEM = ""; 13 | BufferedReader br = new BufferedReader(new FileReader(filename)); 14 | String line; 15 | while ((line = br.readLine()) != null) { 16 | strKeyPEM += line + "\n"; 17 | } 18 | br.close(); 19 | return strKeyPEM; 20 | } 21 | 22 | public static PrivateKey getPrivateKey(String filename) throws IOException, GeneralSecurityException { 23 | String privateKeyPEM = getKey(filename); 24 | return getPrivateKeyFromString(privateKeyPEM); 25 | } 26 | 27 | public static PrivateKey getPrivateKeyFromString(String key) throws IOException, GeneralSecurityException { 28 | String privateKeyPEM = key; 29 | privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----\n", ""); 30 | privateKeyPEM = privateKeyPEM.replace("-----END PRIVATE KEY-----", ""); 31 | privateKeyPEM = privateKeyPEM.replace("\n", ""); 32 | byte[] encoded = Base64.getDecoder().decode(privateKeyPEM); 33 | KeyFactory kf = KeyFactory.getInstance("EC"); 34 | PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); 35 | PrivateKey privKey = (PrivateKey) kf.generatePrivate(keySpec); 36 | return privKey; 37 | } 38 | 39 | public static PublicKey getPublicKey(String filename) throws IOException, GeneralSecurityException { 40 | String publicKeyPEM = getKey(filename); 41 | return getPublicKeyFromString(publicKeyPEM); 42 | } 43 | 44 | public static PublicKey getPublicKeyFromString(String key) throws IOException, GeneralSecurityException { 45 | String publicKeyPEM = key; 46 | publicKeyPEM = publicKeyPEM.replace("-----BEGIN PUBLIC KEY-----\n", ""); 47 | publicKeyPEM = publicKeyPEM.replace("-----END PUBLIC KEY-----", ""); 48 | publicKeyPEM = publicKeyPEM.replace("\n", ""); 49 | byte[] encoded = Base64.getDecoder().decode(publicKeyPEM); 50 | KeyFactory kf = KeyFactory.getInstance("EC"); 51 | PublicKey pubKey = (PublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded)); 52 | return pubKey; 53 | } 54 | 55 | public static BigInteger extractR(byte[] signature) throws Exception { 56 | int startR = (signature[1] & 0x80) != 0 ? 3 : 2; 57 | int lengthR = signature[startR + 1]; 58 | return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR)); 59 | } 60 | 61 | public static BigInteger extractS(byte[] signature) throws Exception { 62 | int startR = (signature[1] & 0x80) != 0 ? 3 : 2; 63 | int lengthR = signature[startR + 1]; 64 | int startS = startR + 2 + lengthR; 65 | int lengthS = signature[startS + 1]; 66 | return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS)); 67 | } 68 | 69 | public static byte[] derSign(BigInteger r, BigInteger s) throws Exception { 70 | byte[] rb = r.toByteArray(); 71 | byte[] sb = s.toByteArray(); 72 | int off = (2 + 2) + rb.length; 73 | int tot = off + (2 - 2) + sb.length; 74 | byte[] der = new byte[tot + 2]; 75 | der[0] = 0x30; 76 | der[1] = (byte) (tot & 0xff); 77 | der[2 + 0] = 0x02; 78 | der[2 + 1] = (byte) (rb.length & 0xff); 79 | System.arraycopy(rb, 0, der, 2 + 2, rb.length); 80 | der[off + 0] = 0x02; 81 | der[off + 1] = (byte) (sb.length & 0xff); 82 | System.arraycopy(sb, 0, der, off + 2, sb.length); 83 | return der; 84 | } 85 | 86 | public static String getSignatureString(byte[] sign) throws Exception { 87 | // System.out.println("Signature: " + new BigInteger(1, sign).toString(16)); 88 | 89 | BigInteger r = extractR(sign); 90 | BigInteger s = extractS(sign); 91 | 92 | // System.out.println(r); 93 | // System.out.println(s); 94 | String realSign = "[" + r.toString() + ", " + s.toString() + "]"; 95 | return realSign; 96 | } 97 | 98 | public static void main(String[] args) throws Exception { 99 | 100 | // Generate a Key Pair 101 | KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC"); 102 | ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); 103 | SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); 104 | keyGen.initialize(ecSpec, random); 105 | KeyPair pair = keyGen.generateKeyPair(); 106 | PrivateKey priv = pair.getPrivate(); 107 | PublicKey pub = pair.getPublic(); 108 | 109 | // Storing the Pub/Priv Key as String 110 | String pubStr = new String(Base64.getEncoder().encode(pub.getEncoded())); 111 | String privStr = new String(Base64.getEncoder().encode(priv.getEncoded())); 112 | 113 | // Restoring Pub/Priv Keys from String 114 | PrivateKey restoredPriv = getPrivateKeyFromString(privStr); 115 | PublicKey restoredPub = getPublicKeyFromString(pubStr); 116 | 117 | // Signing a String 118 | Signature dsa = Signature.getInstance("SHA256withECDSA"); 119 | dsa.initSign(priv); // Pass the private Key that we need. 120 | String str = "VJTI"; // The string that needs to be signed. 121 | byte[] strByte = str.getBytes("UTF-8"); 122 | dsa.update(strByte); 123 | byte[] sign = dsa.sign(); // Actual Signing of the String 'str' with PrivateKey 'priv'. 124 | 125 | // Sending the signature 126 | String realSig = getSignatureString(sign); 127 | System.out.println(realSig); 128 | System.out.println(str); 129 | System.out.println(pubStr); 130 | // Send this signature along with the String that you signed and your pubKey i.e. [realSig, str, pubStr] 131 | } 132 | } -------------------------------------------------------------------------------- /src/views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VJTI Chain 6 | 7 | 8 | 9 | 217 | 218 | 219 | 220 |

ERROR

221 |
222 | 4 223 | 0 224 | 4 225 |
226 | 227 | 228 | -------------------------------------------------------------------------------- /src/views/explorer.html: -------------------------------------------------------------------------------- 1 | % include('header.html', title="explorer") 2 | % from datetime import datetime 3 |
4 |
5 | 13 |
14 |
15 |
16 |
17 | % if len(blocks) > 0: 18 |
19 |
20 |
21 | % for i, block in enumerate(blocks): 22 | % active = "" 23 | % if i == 0: 24 | % active = "active" 25 | % end 26 | {{ str(block)[-12:] }} {{ block.header.height }} 28 | % end 29 |
30 |
31 |
32 | 84 |
85 |
86 | Previous Blocks 87 | % if int(prev) > 0: 88 | Next Blocks 89 | % end 90 | % end 91 | % if len(blocks) <= 0: 92 |
No blocks to show
93 | Show Latest Blocks 94 | % end 95 |
96 |
97 |
98 |
99 | % if len(transactions) > 0: 100 |
Unmined Transactions
101 | 102 | 103 | 104 | 105 | 106 | % for tx in transactions: 107 | % pub, receivers = tx.summarize() 108 | % amount = 0 109 | % for key, amt in receivers.items(): 110 | % amount += amt 111 | % end 112 | 113 | 114 | 115 | 116 | % end 117 |
SenderAmount
{{ pub }}{{ amount }}
118 | % end 119 | % if len(transactions) <= 0: 120 |
No transactions in the Mempool
121 | % end 122 |
123 |
124 |
125 |
126 | 127 | % include('footer.html') -------------------------------------------------------------------------------- /src/Validation Rules.md: -------------------------------------------------------------------------------- 1 | # Transaction Validation 2 | 3 | - Check syntactic correctness `Assumed Done, else error in json decoding` 4 | - Make sure neither in or out lists are empty `Done, Transaction: is_valid()-1` 5 | - Size in bytes <= MAX_BLOCK_SIZE `Done, Transaction: is_valid()-2` 6 | - Each output value, as well as the total, must be in legal money range `Done, Transaction: is_valid()-3` 7 | - Make sure none of the inputs have hash=0, n=-1 (coinbase transactions) `Done, Transaction: isvalid()-4: TxIn: isvalid()` 8 | - Check that nLockTime <= INT_MAX[1], size in bytes >= 100[2], and sig opcount <= 2[3] `Skipping, NA` 9 | - Reject "nonstandard" transactions: scriptSig doing anything other than pushing numbers on the stack, or scriptPubkey not matching the two usual forms[4] `Skipping, NA` 10 | - Reject if we already have matching tx in the pool, or in a block in the main branch `Skipping, TODO in Miner` 11 | - For each input, if the referenced output exists in any other tx in the pool, reject this transaction.[5] `Skipping, TODO in Miner` 12 | - For each input, look in the main branch and the transaction pool to find the referenced output transaction. `Skipping, TODO in Miner` 13 | - If the output transaction is missing for any input, this will be an orphan transaction. Add to the orphan transactions, if a matching transaction is not in there already. `This case should not arise as we dont allow empty TxOut` 14 | - For each input, if the referenced output transaction is coinbase (i.e. only 1 input, with hash=0, n=-1), it must have at least COINBASE_MATURITY (100) confirmations; else reject this transaction `DONE, Chain: is_transaction_valid() ` 15 | - For each input, if the referenced output does not exist (e.g. never existed or has already been spent), reject this transaction[6] `Done, Chain: is_transaction_valid()` 16 | - Using the referenced output transactions to get input values, check that each input value, as well as the sum, are in legal money range `Done, Chain: is_transaction_valid()` 17 | - Reject if the sum of input values < sum of output values `Done, Chain: is_transaction_valid()` 18 | - Reject if transaction fee (defined as sum of input values minus sum of output values) would be too low to get into an empty block `Skipping, TODO in Miner` 19 | - Verify the scriptPubKey accepts for each input; reject if any are bad `Done, Chain: is_transaction_valid()` 20 | - Add to transaction pool[7] `Skipping, TODO in Miner` 21 | - "Add to wallet if mine" `Skipping, TODO in Full Node Flask: newTransaction` 22 | - Relay transaction to peers `Skipping, TODO in Full Node Flask: newTransaction` 23 | - For each orphan transaction that uses this one as one of its inputs, run all these steps (including this one) recursively on that orphan `Skipping, TODO in Full Node Flask: newTransaction` 24 | 25 | # Block Validation 26 | 27 | - Check syntactic correctness `Assumed Done, else error in json decoding` 28 | - Reject if duplicate of block we have in any of the three categories `Not Sure, TODO in FUllNode Flask: GetBlock` 29 | - Transaction list must be non-empty `Done, Block: is_Valid()-1` 30 | - Block hash must satisfy claimed nBits proof of work `Done, Chain: is_block_valid()-2` 31 | - Block timestamp must not be more than two hours in the future `Done, Chain: is_block_valid()-3` 32 | - First transaction must be coinbase (i.e. only 1 input, with hash=0, n=-1), the rest must not be `Done, Block: is_valid()-2` 33 | - For each transaction, apply "tx" checks 2-4 `Done, Block: is_valid()-3` 34 | - For the coinbase (first) transaction, scriptSig length must be 2-100 `Skipping, NA` 35 | - Reject if sum of transaction sig opcounts > MAX_BLOCK_SIGOPS `Skipping, NA` 36 | - Verify Merkle hash `Done, Block: is_valid()-4` 37 | - Check if prev block (matching prev hash) is in main branch or side branches. If not, add this to orphan blocks, then query peer we got this from for 1st missing orphan block in prev chain; done with block `Partially Done, Chain: is_block_valid()-4| TODO Flask: newBlock` 38 | - Check that nBits value matches the difficulty rules `Done, Chain: is_block_valid()-2` 39 | - Reject if timestamp is the median time of the last 11 blocks or before `Done, Chain: is_block_valid() - 5` 40 | - For certain old blocks (i.e. on initial block download) check that hash matches known values `No Idea, TODO? getBlock? Initial sync??` 41 | ---- 42 | **¯\\_(ツ)_/¯** 43 | - Add block into the tree. There are three cases: 1. block further extends the main branch; 2. block extends a side branch but does not add enough difficulty to make it become the new main branch; 3. block extends a side branch and makes it the new main branch. ` ` 44 | - For case 1, adding to main branch: ` ` 45 | - For all but the coinbase transaction, apply the following: ` ` 46 | - For each input, look in the main branch to find the referenced output transaction. Reject if the output transaction is missing for any input. ` ` 47 | - For each input, if we are using the nth output of the earlier transaction, but it has fewer than n+1 outputs, reject. ` ` 48 | - For each input, if the referenced output transaction is coinbase (i.e. only 1 input, with hash=0, n=-1), it must have at least COINBASE_MATURITY (100) confirmations; else reject. ` ` 49 | - Verify crypto signatures for each input; reject if any are bad ` ` 50 | - For each input, if the referenced output has already been spent by a transaction in the main branch, reject ` ` 51 | - Using the referenced output transactions to get input values, check that each input value, as well as the sum, are in legal money range ` ` 52 | - Reject if the sum of input values < sum of output values ` ` 53 | - Reject if coinbase value > sum of block creation fee and transaction fees ` ` 54 | (If we have not rejected): ` ` 55 | - For each transaction, "Add to wallet if mine" ` ` 56 | - For each transaction in the block, delete any matching transaction from the transaction pool ` ` 57 | - Relay block to our peers ` ` 58 | - If we rejected, the block is not counted as part of the main branch ` ` 59 | - For case 2, adding to a side branch, we don't do anything. ` ` 60 | - For case 3, a side branch becoming the main branch: ` ` 61 | - Find the fork block on the main branch which this side branch forks off of ` ` 62 | - Redefine the main branch to only go up to this fork block ` ` 63 | - For each block on the side branch, from the child of the fork block to the leaf, add to the main branch: ` ` 64 | - Do "branch" checks 3-11 ` ` 65 | - For all but the coinbase transaction, apply the following: ` ` 66 | - For each input, look in the main branch to find the referenced output transaction. Reject if the output transaction is missing for any input. ` ` 67 | - For each input, if we are using the nth output of the earlier transaction, but it has fewer than n+1 outputs, reject. ` ` 68 | - For each input, if the referenced output transaction is coinbase (i.e. only 1 input, with hash=0, n=-1), it must have at least COINBASE_MATURITY (100) confirmations; else reject. ` ` 69 | - Verify crypto signatures for each input; reject if any are bad ` ` 70 | - For each input, if the referenced output has already been spent by a transaction in the main branch, reject ` ` 71 | - Using the referenced output transactions to get input values, check that each input value, as well as the sum, are in legal money range ` ` 72 | - Reject if the sum of input values < sum of output values ` ` 73 | - Reject if coinbase value > sum of block creation fee and transaction fees ` ` 74 | (If we have not rejected): ` ` 75 | - For each transaction, "Add to wallet if mine" ` ` 76 | - If we reject at any point, leave the main branch as what it was originally, done with block ` ` 77 | - For each block in the old main branch, from the leaf down to the child of the fork block: ` ` 78 | - For each non-coinbase transaction in the block: ` ` 79 | - Apply "tx" checks 2-9, except in step 8, only look in the transaction pool for duplicates, not the main branch ` ` 80 | - Add to transaction pool if accepted, else go on to next transaction ` ` 81 | - For each block in the new main branch, from the child of the fork node to the leaf: ` ` 82 | - For each transaction in the block, delete any matching transaction from the transaction pool ` ` 83 | - Relay block to our peers ` ` 84 | - For each orphan block for which this block is its prev, run all these steps (including this one) recursively on that orphan ` ` 85 | -------------------------------------------------------------------------------- /src/core.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from datetime import datetime 4 | from collections import Counter 5 | from dataclasses import dataclass, field 6 | from operator import attrgetter 7 | from statistics import median 8 | from sys import getsizeof 9 | from threading import RLock 10 | from typing import Any, Dict, List, Optional, Set 11 | 12 | import utils.constants as consts 13 | from utils.dataclass_json import DataClassJson 14 | from utils.logger import logger 15 | from utils.storage import add_block_to_db, check_block_in_db, get_block_from_db, remove_block_from_db, write_header_list_to_db 16 | from utils.utils import dhash, get_time_difference_from_now_secs, lock, merkle_hash, generate_tx_hist 17 | from wallet import Wallet 18 | from authority_rules import authority_rules 19 | from collections import deque 20 | 21 | 22 | @dataclass 23 | class SingleOutput(DataClassJson): 24 | """ References a single output """ 25 | 26 | # The transaction id which contains this output 27 | txid: str 28 | 29 | # The index of this output in the transaction 30 | vout: int 31 | 32 | 33 | @dataclass 34 | class TxOut(DataClassJson, dict): 35 | """ A single Transaction Output """ 36 | 37 | # The amount in scoin 38 | amount: int 39 | 40 | # Public key hash of receiver in pubkey script 41 | address: str 42 | 43 | 44 | @dataclass 45 | class TxIn(DataClassJson, dict): 46 | """ A single Transaction Input """ 47 | 48 | # The UTXO we will be spending 49 | # Can be None for coinbase tx 50 | payout: Optional[SingleOutput] 51 | 52 | # Signature and public key in the scriptSig 53 | sig: str 54 | pub_key: str 55 | 56 | # Check if the TxIn is Valid 57 | def is_valid(self) -> bool: 58 | try: 59 | # Ensure the Transaction Id is valid hex string 60 | if not len(self.payout.txid or "") == consts.HASH_LENGTH_HEX: 61 | logger.debug("TxIn: TxID of invalid length") 62 | return False 63 | # Ensure the payment index is valid 64 | if not int(self.payout.vout) >= 0: 65 | logger.debug("TxIn: Payment index(vout) invalid") 66 | return False 67 | # Ensure the sig and pubkey are valid 68 | if len(self.sig or "") == 0 or len(self.pub_key or "") == 0: 69 | logger.debug("TxIN: Sig/Pubkey of invalid length") 70 | return False 71 | except Exception as e: 72 | logger.error(e) 73 | return False 74 | return True 75 | 76 | 77 | @dataclass 78 | class Transaction(DataClassJson): 79 | """ A transaction as defined by bitcoin core """ 80 | 81 | def __str__(self): 82 | return self.to_json() 83 | 84 | def __hash__(self): 85 | return int(dhash(self), 16) 86 | 87 | def __eq__(self, other): 88 | attrs_sam = self.version == other.version 89 | attrs_same = attrs_sam and self.timestamp == other.timestamp and self.locktime == other.locktime 90 | txin_same = True 91 | for txin in self.vin.values(): 92 | if txin not in other.vin.values(): 93 | txin_same = False 94 | break 95 | txout_same = True 96 | for txout in self.vout.values(): 97 | if txout not in other.vout.values(): 98 | txout_same = False 99 | break 100 | return attrs_same and txin_same and txout_same 101 | 102 | def hash(self): 103 | return dhash(self) 104 | 105 | def sign(self, w=None): 106 | sign_copy_of_tx = copy.deepcopy(self) 107 | sign_copy_of_tx.vin = {} 108 | sig = w.sign(sign_copy_of_tx.to_json()) 109 | for i in self.vin: 110 | self.vin[i].sig = sig 111 | 112 | def add_sign(self, sig): 113 | for i in self.vin: 114 | self.vin[i].sig = sig 115 | 116 | def summarize(self): 117 | # Summarize the transaction and give sender address, receiver addresses and amount. 118 | pub_key = "SomePublicKey" 119 | receivers = {} 120 | for i in self.vin: 121 | pub_key = self.vin[i].pub_key 122 | 123 | for i in self.vout: 124 | address = self.vout[i].address 125 | if address == pub_key: 126 | continue 127 | if address not in receivers: 128 | receivers[address] = 0 129 | receivers[address] += self.vout[i].amount 130 | return pub_key, receivers 131 | 132 | def is_valid(self): 133 | # No empty inputs or outputs -1 134 | if len(self.vin) == 0 or len(self.vout) == 0: 135 | logger.debug("Transaction: Empty vin/vout") 136 | return False 137 | 138 | # Transaction size should not exceed max block size -2 139 | if getsizeof(str(self)) > consts.MAX_BLOCK_SIZE_KB * 1024: 140 | logger.debug("Transaction: Size Exceeded") 141 | return False 142 | 143 | # All outputs in legal money range -3 144 | for index, out in self.vout.items(): 145 | if out.amount > consts.MAX_COINS_POSSIBLE or out.amount <= 0: 146 | logger.debug("Transaction: Invalid Amount" + str(out.amount)) 147 | return False 148 | 149 | # Verify all Inputs are valid - 4 150 | for index, inp in self.vin.items(): 151 | if not inp.is_valid(): 152 | logger.debug("Transaction: Invalid TxIn") 153 | return False 154 | 155 | # Verify locktime -5 156 | difference = get_time_difference_from_now_secs(self.locktime) 157 | if difference > 0: 158 | logger.debug("Transaction: Locktime Verify Failed") 159 | return False 160 | 161 | # Limit Message size 162 | if len(self.message) > consts.MAX_MESSAGE_SIZE: 163 | logger.debug("Transaction: Message exceeds allowed length") 164 | return False 165 | return True 166 | 167 | def object(self): 168 | newtransaction = copy.deepcopy(self) 169 | n_vin = {} 170 | for j, tx_in in self.vin.items(): 171 | if not isinstance(tx_in, TxIn): 172 | n_vin[int(j)] = TxIn.from_json(json.dumps(tx_in)) 173 | else: 174 | n_vin[int(j)] = copy.deepcopy(tx_in) 175 | 176 | n_vout = {} 177 | for j, tx_out in self.vout.items(): 178 | if not isinstance(tx_out, TxOut): 179 | n_vout[int(j)] = TxOut.from_json(json.dumps(tx_out)) 180 | else: 181 | n_vout[int(j)] = copy.deepcopy(tx_out) 182 | 183 | newtransaction.vin = n_vin 184 | newtransaction.vout = n_vout 185 | 186 | return newtransaction 187 | 188 | # Version for this transaction 189 | version: int 190 | 191 | # Timestamp for this transaction 192 | timestamp: int 193 | 194 | # Earliest time(Unix timestamp >500000000) 195 | # when this transaction may be added to the block chain. 196 | # -1 for coinbase transaction 197 | locktime: int 198 | 199 | # The input transactions 200 | vin: Dict[int, TxIn] 201 | 202 | # The output transactions 203 | vout: Dict[int, TxOut] 204 | 205 | # Message associated with this transaction 206 | message: str = "" 207 | 208 | 209 | @dataclass 210 | class BlockHeader(DataClassJson): 211 | """ The header of a block """ 212 | 213 | # Version 214 | version: int 215 | 216 | # Block Height 217 | height: Optional[int] = field(repr=False) 218 | 219 | # A reference to the hash of the previous block 220 | prev_block_hash: Optional[str] 221 | 222 | # A hash of the root of the merkle tree of this block’s transactions 223 | merkle_root: str 224 | 225 | # The approximate creation time of this block (seconds from Unix Epoch) 226 | timestamp: int 227 | 228 | # Signature of the authority who mined this block 229 | signature: str 230 | 231 | 232 | @dataclass 233 | class Block(DataClassJson): 234 | """ A single block """ 235 | 236 | # The block header 237 | header: BlockHeader 238 | 239 | # The transactions in this block 240 | transactions: List[Transaction] 241 | 242 | # Validate object 243 | def object(self): 244 | newblock = copy.deepcopy(self) 245 | for i, tx in enumerate(self.transactions): 246 | newblock.transactions[i] = self.transactions[i].object() 247 | return newblock 248 | 249 | def __repr__(self): 250 | return dhash(self.header) 251 | 252 | def is_valid(self) -> bool: 253 | # Block should be of valid size and List of Transactions should not be empty -1 254 | if getsizeof(self.to_json()) > consts.MAX_BLOCK_SIZE_KB * 1024 or len(self.transactions) == 0: 255 | logger.debug("Block: Size Exceeded/No. of Tx==0") 256 | return False 257 | 258 | # Make sure each transaction is valid -3 259 | for tx in self.transactions: 260 | if not tx.is_valid(): 261 | logger.debug("Block: Transaction is not Valid") 262 | return False 263 | 264 | # Verify merkle hash -4 265 | if self.header.merkle_root != merkle_hash(self.transactions): 266 | logger.debug("Block: Merkle Hash failed") 267 | return False 268 | return True 269 | 270 | 271 | @dataclass 272 | class Utxo: 273 | # Mapping from string repr of SingleOutput to List[TxOut, Blockheader] 274 | utxo: Dict[str, List[Any]] = field(default_factory=dict) 275 | 276 | def get(self, so: SingleOutput) -> Optional[List[Any]]: 277 | so_str = so.to_json() 278 | if so_str in self.utxo: 279 | return self.utxo[so_str] 280 | return None, None, None 281 | 282 | def set(self, so: SingleOutput, txout: TxOut, blockheader: BlockHeader): 283 | so_str = so.to_json() 284 | self.utxo[so_str] = [txout, blockheader] 285 | 286 | def remove(self, so: SingleOutput) -> bool: 287 | so_str = so.to_json() 288 | if so_str in self.utxo: 289 | del self.utxo[so_str] 290 | return True 291 | return False 292 | 293 | 294 | @dataclass 295 | class TxHistory: 296 | tx_hist: Dict = field(default_factory=dict) 297 | 298 | def append(self, pub_key: str, tx: str) -> None: 299 | if pub_key not in self.tx_hist: 300 | self.tx_hist[pub_key] = deque(maxlen=consts.MAX_TRANSACTION_HISTORY_TO_KEEP) 301 | self.tx_hist[pub_key].append(tx) 302 | return 303 | 304 | def get(self, pub_key: str) -> List[str]: 305 | if pub_key in self.tx_hist: 306 | return list(reversed(self.tx_hist[pub_key])) 307 | return [] 308 | 309 | 310 | @dataclass 311 | class Chain: 312 | # The max length of the blockchain 313 | length: int = 0 314 | 315 | # The list of blocks 316 | header_list: List[BlockHeader] = field(default_factory=list) 317 | 318 | # The UTXO Set 319 | utxo: Utxo = field(default_factory=Utxo) 320 | 321 | # Transaction History 322 | transaction_history: TxHistory = field(default_factory=TxHistory) 323 | 324 | def __eq__(self, other): 325 | for i, h in enumerate(self.header_list): 326 | if dhash(h) != dhash(other.header_list[i]): 327 | return False 328 | return True 329 | 330 | @classmethod 331 | def build_from_header_list(cls, hlist: List[BlockHeader]): 332 | nchain = cls() 333 | nchain.header_list = [] 334 | for header in hlist: 335 | block = Block.from_json(get_block_from_db(dhash(header))).object() 336 | nchain.add_block(block) 337 | return nchain 338 | 339 | # Build the UTXO Set from scratch 340 | def build_utxo(self): 341 | for header in self.header_list: 342 | block = Block.from_json(get_block_from_db(dhash(header))).object() 343 | self.update_utxo(block) 344 | 345 | # Update the UTXO Set on adding new block, *Assuming* the block being added is valid 346 | def update_utxo(self, block: Block): 347 | block_transactions: List[Transaction] = block.transactions 348 | for t in block_transactions: 349 | thash = dhash(t) 350 | # Remove the spent outputs 351 | for tinput in t.vin: 352 | so = t.vin[tinput].payout 353 | if so: 354 | self.utxo.remove(so) 355 | # Add new unspent outputs 356 | for touput in t.vout: 357 | self.utxo.set(SingleOutput(txid=thash, vout=touput), t.vout[touput], block.header) 358 | 359 | def is_transaction_valid(self, transaction: Transaction): 360 | if not transaction.is_valid(): 361 | return False 362 | 363 | sum_of_all_inputs = 0 364 | sum_of_all_outputs = 0 365 | sign_copy_of_tx = copy.deepcopy(transaction) 366 | sign_copy_of_tx.vin = {} 367 | for inp, tx_in in transaction.vin.items(): 368 | tx_out, block_hdr = self.utxo.get(tx_in.payout) 369 | # ensure the TxIn is present in utxo, i.e exists and has not been spent 370 | if block_hdr is None: 371 | logger.debug(tx_in.payout) 372 | logger.debug("Chain: Transaction not present in utxo") 373 | return False 374 | 375 | # Verify that the Signature is valid for all inputs 376 | if not Wallet.verify(sign_copy_of_tx.to_json(), tx_in.sig, tx_out.address): 377 | logger.debug("Chain: Invalid Signature") 378 | return False 379 | 380 | sum_of_all_inputs += tx_out.amount 381 | 382 | if sum_of_all_inputs > consts.MAX_COINS_POSSIBLE or sum_of_all_inputs < 0: 383 | logger.debug("Chain: Invalid input Amount") 384 | return False 385 | 386 | for out, tx in transaction.vout.items(): 387 | sum_of_all_outputs += tx.amount 388 | 389 | # ensure sum of amounts of all inputs is in valid amount range 390 | if sum_of_all_outputs > consts.MAX_COINS_POSSIBLE or sum_of_all_outputs < 0: 391 | logger.debug("Chain: Invalid output Amount") 392 | return False 393 | 394 | # ensure sum of amounts of all inputs is > sum of amounts of all outputs 395 | if not sum_of_all_inputs == sum_of_all_outputs: 396 | logger.debug("Chain: input sum less than output sum") 397 | return False 398 | 399 | return True 400 | 401 | def is_block_valid(self, block: Block): 402 | # Check if the block is valid -1 403 | 404 | local_utxo = copy.deepcopy(self.utxo) 405 | 406 | if not block.is_valid(): 407 | logger.debug("Block is not valid") 408 | return False 409 | 410 | # Ensure the prev block header matches the previous block hash in the Chain -4 411 | if len(self.header_list) > 0 and not dhash(self.header_list[-1]) == block.header.prev_block_hash: 412 | logger.debug("Chain: Block prev header does not match previous block") 413 | return False 414 | 415 | # Validating each transaction in block 416 | for t in block.transactions: 417 | if self.is_transaction_valid(t): 418 | thash = dhash(t) 419 | # Remove the spent outputs 420 | for tinput in t.vin: 421 | so = t.vin[tinput].payout 422 | if so: 423 | if local_utxo.get(so)[0] is not None: 424 | local_utxo.remove(so) 425 | else: 426 | logger.error("Chain: Single output missing in UTxO, Transaction invalid") 427 | return False 428 | else: 429 | logger.error("Chain: No Single output, Transaction invalid") 430 | return False 431 | # Add new unspent outputs 432 | for touput in t.vout: 433 | local_utxo.set(SingleOutput(txid=thash, vout=touput), t.vout[touput], block.header) 434 | else: 435 | logger.debug("Chain: Transaction not valid") 436 | return False 437 | 438 | # Validate Authority Signature 439 | timestamp = datetime.fromtimestamp(block.header.timestamp) 440 | seconds_since_midnight = (timestamp - timestamp.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() 441 | for authority in authority_rules["authorities"]: 442 | if seconds_since_midnight <= authority["to"] and seconds_since_midnight >= authority["from"]: 443 | blk_hdr = copy.deepcopy(block.header) 444 | blk_hdr.signature = "" 445 | if Wallet.verify(dhash(blk_hdr), block.header.signature, authority["pubkey"]): 446 | return True 447 | return False 448 | 449 | def add_block(self, block: Block, is_genesis: bool) -> bool: 450 | if is_genesis or self.is_block_valid(block): 451 | self.header_list.append(block.header) 452 | self.update_utxo(block) 453 | self.length = len(self.header_list) 454 | add_block_to_db(block) 455 | for tx in block.transactions: 456 | pub_key, data = tx.summarize() 457 | for address in data: 458 | amount = data[address] 459 | timestamp = tx.timestamp 460 | bhash = dhash(block.header) 461 | thash = dhash(tx) 462 | message = tx.message 463 | history = generate_tx_hist(amount, pub_key, timestamp, bhash, thash, message) 464 | self.transaction_history.append(address, history) 465 | 466 | history = generate_tx_hist(-amount, address, timestamp, bhash, thash, message) 467 | self.transaction_history.append(pub_key, history) 468 | 469 | logger.info("Chain: Added Block " + str(block)) 470 | return True 471 | return False 472 | 473 | 474 | class BlockChain: 475 | 476 | block_lock = RLock() 477 | 478 | def __init__(self): 479 | self.active_chain: Chain = Chain() 480 | self.mempool: Set[Transaction] = set() 481 | 482 | def remove_transactions_from_mempool(self, block: Block): 483 | """Removes transaction from the mempool based on a new received block 484 | 485 | Arguments: 486 | block {Block} -- The block which is received 487 | """ 488 | new_mempool = set() 489 | for x in self.mempool: 490 | DONE = True 491 | for t in block.transactions: 492 | if dhash(x) == dhash(t): 493 | DONE = False 494 | if DONE: 495 | new_mempool.add(x) 496 | self.mempool = new_mempool 497 | 498 | def update_active_chain(self): 499 | # Save Active Chain to DB 500 | write_header_list_to_db(self.active_chain.header_list) 501 | 502 | def build_from_header_list(self, hlist: List[str]): 503 | try: 504 | for header in hlist: 505 | block = Block.from_json(get_block_from_db(header)).object() 506 | if block: 507 | self.add_block(block) 508 | else: 509 | logger.error("Blockchain: Block does not exist in DB") 510 | except Exception as e: 511 | logger.error("Blockchain: Exception " + str(e)) 512 | 513 | @lock(block_lock) 514 | def add_block(self, block: Block): 515 | blockAdded = False 516 | 517 | chain = self.active_chain 518 | is_genesis = chain.length == 0 519 | if is_genesis or block.header.prev_block_hash == dhash(chain.header_list[-1]): 520 | if chain.add_block(block, is_genesis): 521 | self.update_active_chain() 522 | self.remove_transactions_from_mempool(block) 523 | blockAdded = True 524 | 525 | return blockAdded 526 | 527 | 528 | genesis_block_transaction = [ 529 | Transaction( 530 | version=1, 531 | locktime=0, 532 | timestamp=1551698572, 533 | message="Genesis Transaction", 534 | vin={0: TxIn(payout=None, sig=consts.GENESIS_BLOCK_SIGNATURE, pub_key="Genesis")}, 535 | vout={ 536 | 0: TxOut( 537 | amount=1000000, 538 | address="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0tmjDG6v51ELMieRGuTfOgmfTe7BzNBsHQseqygX58+MQjNyjoOPkphghhYFpIFPzVORAI6Qief9lrncuWsOMg==", 539 | ), 540 | 1: TxOut( 541 | amount=1000000, 542 | address="MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3s5Iqp9VzlL7ngLfR2xb1RIGfuo+siL/zaZdeFblI8pnU5SpJCFEEMZDQBnEEPIOz9bv9lK46AwV3vLcN1VpCA==", 543 | ), 544 | }, 545 | ) 546 | ] 547 | 548 | 549 | genesis_block_header = BlockHeader( 550 | version=1, 551 | prev_block_hash=None, 552 | height=0, 553 | merkle_root=merkle_hash(genesis_block_transaction), 554 | timestamp=1551698580, 555 | signature="", 556 | ) 557 | genesis_block = Block(header=genesis_block_header, transactions=genesis_block_transaction) 558 | 559 | 560 | if __name__ == "__main__": 561 | print(genesis_block) 562 | logger.debug(genesis_block) 563 | gb_json = genesis_block.to_json() 564 | gb = Block.from_json(gb_json).object() 565 | print(gb.transactions[0].vout[0].amount) 566 | -------------------------------------------------------------------------------- /src/fullnode.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | from functools import lru_cache 4 | from multiprocessing import Pool, Process 5 | from threading import Thread, Timer 6 | from typing import Any, Dict, List 7 | from datetime import datetime 8 | import hashlib 9 | import inspect 10 | import requests 11 | import waitress 12 | from bottle import BaseTemplate, Bottle, request, response, static_file, template, error 13 | 14 | import utils.constants as consts 15 | from core import Block, BlockChain, SingleOutput, Transaction, TxIn, TxOut, genesis_block 16 | from authority import Authority 17 | from utils.logger import logger, iplogger 18 | from utils.storage import get_block_from_db, get_wallet_from_db, read_header_list_from_db 19 | from utils.utils import compress, decompress, dhash 20 | from wallet import Wallet 21 | 22 | app = Bottle() 23 | BaseTemplate.defaults["get_url"] = app.get_url 24 | 25 | LINE_PROFILING = False 26 | 27 | BLOCKCHAIN = BlockChain() 28 | 29 | PEER_LIST: List[Dict[str, Any]] = [] 30 | 31 | MY_WALLET = Wallet() 32 | 33 | miner = Authority() 34 | 35 | 36 | def mining_thread_task(): 37 | while True: 38 | if not miner.is_mining() and not consts.NO_MINING: 39 | try: 40 | miner.start_mining(BLOCKCHAIN.mempool, BLOCKCHAIN.active_chain, MY_WALLET) 41 | except Exception as e: 42 | miner.stop_mining() 43 | logger.debug("Miner: Error while mining:" + str(e)) 44 | time.sleep(consts.MINING_INTERVAL_THRESHOLD // 2) 45 | 46 | 47 | def send_to_all_peers(url, data): 48 | def request_task(peers, url, data): 49 | for peer in peers: 50 | try: 51 | requests.post(get_peer_url(peer) + url, data=data, timeout=(5, 1)) 52 | except Exception as e: 53 | logger.debug("Server: Requests: Error while sending data in process" + str(peer)) 54 | 55 | Process(target=request_task, args=(PEER_LIST, url, data), daemon=True).start() 56 | 57 | 58 | def start_mining_thread(): 59 | time.sleep(5) 60 | Thread(target=mining_thread_task, name="Miner", daemon=True).start() 61 | 62 | 63 | def fetch_peer_list() -> List[Dict[str, Any]]: 64 | try: 65 | r = requests.post(consts.SEED_SERVER_URL, data={"port": consts.MINER_SERVER_PORT}) 66 | peer_list = json.loads(r.text) 67 | return peer_list 68 | except Exception as e: 69 | logger.error("Could not connect to DNS Seed") 70 | return [] 71 | 72 | 73 | def get_peer_url(peer: Dict[str, Any]) -> str: 74 | return "http://" + str(peer["ip"]) + ":" + str(peer["port"]) 75 | 76 | 77 | def greet_peer(peer: Dict[str, Any]) -> bool: 78 | try: 79 | url = get_peer_url(peer) 80 | data = {"port": consts.MINER_SERVER_PORT, "version": consts.MINER_VERSION, "blockheight": BLOCKCHAIN.active_chain.length} 81 | # Send a POST request to the peer 82 | r = requests.post(url + "/greetpeer", data=data) 83 | data = json.loads(r.text) 84 | # Update the peer data in the peer list with the new data received from the peer. 85 | if data.get("blockheight", None): 86 | peer.update(data) 87 | else: 88 | logger.debug("Main: Peer data does not have Block Height") 89 | return False 90 | return True 91 | except Exception as e: 92 | logger.debug("Main: Could not greet peer" + str(e)) 93 | return False 94 | 95 | 96 | def receive_block_from_peer(peer: Dict[str, Any], header_hash) -> Block: 97 | r = requests.post(get_peer_url(peer) + "/getblock", data={"headerhash": header_hash}) 98 | return Block.from_json(decompress(r.text)).object() 99 | 100 | 101 | def check_block_with_peer(peer, hhash): 102 | r = requests.post(get_peer_url(peer) + "/checkblock", data={"headerhash": hhash}) 103 | result = json.loads(r.text) 104 | if result: 105 | return True 106 | return False 107 | 108 | 109 | def get_block_header_hash(height): 110 | return dhash(BLOCKCHAIN.active_chain.header_list[height]) 111 | 112 | 113 | def sync(max_peer): 114 | fork_height = BLOCKCHAIN.active_chain.length 115 | r = requests.post(get_peer_url(max_peer) + "/getblockhashes", data={"myheight": fork_height}) 116 | hash_list = json.loads(decompress(r.text.encode())) 117 | for hhash in hash_list: 118 | block = receive_block_from_peer(max_peer, hhash) 119 | if not BLOCKCHAIN.add_block(block): 120 | logger.error("Sync: Block received is invalid, Cannot Sync") 121 | break 122 | return 123 | 124 | 125 | # Periodically sync with all the peers 126 | def sync_with_peers(): 127 | try: 128 | PEER_LIST = fetch_peer_list() 129 | new_peer_list = [] 130 | for peer in PEER_LIST: 131 | if greet_peer(peer): 132 | new_peer_list.append(peer) 133 | PEER_LIST = new_peer_list 134 | 135 | if PEER_LIST: 136 | max_peer = max(PEER_LIST, key=lambda k: k["blockheight"]) 137 | logger.debug(f"Sync: Syncing with {get_peer_url(max_peer)}, he seems to have height {max_peer['blockheight']}") 138 | sync(max_peer) 139 | except Exception as e: 140 | logger.error("Sync: Error: " + str(e)) 141 | Timer(consts.MINING_INTERVAL_THRESHOLD * 2, sync_with_peers).start() 142 | 143 | 144 | def check_balance(pub_key: str) -> int: 145 | current_balance = 0 146 | for x, utxo_list in BLOCKCHAIN.active_chain.utxo.utxo.items(): 147 | tx_out = utxo_list[0] 148 | if tx_out.address == pub_key: 149 | current_balance += int(tx_out.amount) 150 | return int(current_balance) 151 | 152 | 153 | def send_bounty(receiver_public_keys: List[str], amounts: List[int]): 154 | current_balance = check_balance(MY_WALLET.public_key) 155 | for key in receiver_public_keys: 156 | if len(key) < consts.PUBLIC_KEY_LENGTH: 157 | logger.debug("Invalid Public Key Length") 158 | return False 159 | total_amount = sum(amounts) 160 | if current_balance < total_amount: 161 | logger.debug("Insuficient balance") 162 | elif MY_WALLET.public_key in receiver_public_keys: 163 | logger.debug("Cannot send to myself") 164 | else: 165 | transaction = create_transaction(receiver_public_keys, amounts, MY_WALLET.public_key, message="Authority: Faucet Money") 166 | transaction.sign(MY_WALLET) 167 | logger.info("Wallet: Attempting to Send Transaction") 168 | try: 169 | r = requests.post( 170 | "http://0.0.0.0:" + str(consts.MINER_SERVER_PORT) + "/newtransaction", 171 | data=compress(transaction.to_json()), 172 | timeout=(5, 1), 173 | ) 174 | if r.status_code == 400: 175 | logger.info("Wallet: Could not Send Transaction. Invalid Transaction") 176 | else: 177 | logger.info("Wallet: Transaction Sent, Wait for it to be Mined") 178 | return True 179 | except Exception as e: 180 | logger.error("Wallet: Could not Send Transaction. Try Again." + str(e)) 181 | return False 182 | 183 | 184 | def create_transaction(receiver_public_keys: List[str], amounts: List[int], sender_public_key, message="") -> Transaction: 185 | vout = {} 186 | vin = {} 187 | current_amount = 0 188 | total_amount = sum(amounts) 189 | i = 0 190 | for so, utxo_list in BLOCKCHAIN.active_chain.utxo.utxo.items(): 191 | tx_out = utxo_list[0] 192 | if current_amount >= total_amount: 193 | break 194 | if tx_out.address == sender_public_key: 195 | current_amount += tx_out.amount 196 | vin[i] = TxIn(payout=SingleOutput.from_json(so), pub_key=sender_public_key, sig="") 197 | i += 1 198 | 199 | for i, address in enumerate(receiver_public_keys): 200 | vout[i] = TxOut(amount=amounts[i], address=address) 201 | change = (current_amount - total_amount) 202 | if change > 0: 203 | vout[i + 1] = TxOut(amount=change, address=sender_public_key) 204 | 205 | tx = Transaction(version=consts.MINER_VERSION, locktime=0, timestamp=int(time.time()), vin=vin, vout=vout, message=message) 206 | return tx 207 | 208 | 209 | def get_ip(request): 210 | return request.environ.get("HTTP_X_FORWARDED_FOR") or request.environ.get("REMOTE_ADDR") 211 | 212 | 213 | def log_ip(request, fname): 214 | client_ip = get_ip(request) 215 | iplogger.info(f"{client_ip} : Called function {fname}") 216 | 217 | 218 | @app.post("/checkBalance") 219 | def checkingbalance(): 220 | log_ip(request, inspect.stack()[0][3]) 221 | data = request.json 222 | public_key = data["public_key"] 223 | logger.debug(public_key) 224 | current_balance = check_balance(public_key) 225 | return str(current_balance) 226 | 227 | 228 | @app.post("/makeTransaction") 229 | def make_transaction(): 230 | log_ip(request, inspect.stack()[0][3]) 231 | data = request.json 232 | 233 | bounty = int(data["bounty"]) 234 | receiver_public_key = data["receiver_public_key"] 235 | sender_public_key = data["sender_public_key"] 236 | message = "No Message" 237 | if "message" in data: 238 | message = data["message"] 239 | 240 | if len(receiver_public_key) < consts.PUBLIC_KEY_LENGTH: 241 | logger.debug("Invalid Receiver Public Key") 242 | response.status = 400 243 | return "Invalid Receiver Public Key" 244 | 245 | current_balance = check_balance(sender_public_key) 246 | 247 | if current_balance < bounty: 248 | logger.debug("Insufficient Balance to make Transaction") 249 | response.status = 400 250 | return "Insufficient Balance to make Transaction, need more " + str(bounty - current_balance) 251 | elif sender_public_key == receiver_public_key: 252 | logger.debug("Someone trying to send money to himself") 253 | response.status = 400 254 | return "Cannot send money to youself" 255 | else: 256 | transaction = create_transaction([receiver_public_key], [bounty], sender_public_key, message=message) 257 | data = {} 258 | data["send_this"] = transaction.to_json() 259 | transaction.vin = {} 260 | data["sign_this"] = transaction.to_json() 261 | return json.dumps(data) 262 | 263 | 264 | @app.post("/sendTransaction") 265 | def send_transaction(): 266 | log_ip(request, inspect.stack()[0][3]) 267 | data = request.json 268 | transaction = Transaction.from_json(data["transaction"]).object() 269 | sig = data["signature"] 270 | transaction.add_sign(sig) 271 | 272 | logger.debug(transaction) 273 | logger.info("Wallet: Attempting to Send Transaction") 274 | try: 275 | r = requests.post( 276 | "http://0.0.0.0:" + str(consts.MINER_SERVER_PORT) + "/newtransaction", 277 | data=compress(transaction.to_json()), 278 | timeout=(5, 1), 279 | ) 280 | if r.status_code == 400: 281 | response.status = 400 282 | logger.error("Wallet: Could not Send Transaction. Invalid transaction") 283 | return "Try Again" 284 | except Exception as e: 285 | response.status = 400 286 | logger.error("Wallet: Could not Send Transaction. Try Again." + str(e)) 287 | return "Try Again" 288 | else: 289 | logger.info("Wallet: Transaction Sent, Wait for it to be Mined") 290 | return "Done" 291 | 292 | 293 | @app.post("/transactionHistory") 294 | def transaction_history(): 295 | log_ip(request, inspect.stack()[0][3]) 296 | data = request.json 297 | public_key = data["public_key"] 298 | tx_hist = BLOCKCHAIN.active_chain.transaction_history.get(public_key) 299 | return json.dumps(tx_hist) 300 | 301 | 302 | @app.post("/greetpeer") 303 | def greet_peer_f(): 304 | log_ip(request, inspect.stack()[0][3]) 305 | try: 306 | peer = {} 307 | peer["port"] = request.forms.get("port") 308 | peer["ip"] = request.remote_addr 309 | peer["time"] = time.time() 310 | peer["version"] = request.forms.get("version") 311 | peer["blockheight"] = request.forms.get("blockheight") 312 | 313 | ADD_ENTRY = True 314 | for entry in PEER_LIST: 315 | ip = entry["ip"] 316 | port = entry["port"] 317 | if ip == peer["ip"] and port == peer["port"]: 318 | ADD_ENTRY = False 319 | if ADD_ENTRY: 320 | PEER_LIST.append(peer) 321 | logger.debug("Server: Greet, A new peer joined, Adding to List") 322 | except Exception as e: 323 | logger.debug("Server: Greet Error: " + str(e)) 324 | pass 325 | 326 | data = {"version": consts.MINER_VERSION, "blockheight": BLOCKCHAIN.active_chain.length} 327 | response.content_type = "application/json" 328 | return json.dumps(data) 329 | 330 | 331 | @lru_cache(maxsize=128) 332 | def cached_get_block(headerhash: str) -> str: 333 | if headerhash: 334 | db_block = get_block_from_db(headerhash) 335 | if db_block: 336 | return compress(db_block) 337 | else: 338 | logger.error("ERROR CALLED GETBLOCK FOR NON EXISTENT BLOCK") 339 | return "Invalid Hash" 340 | 341 | 342 | @app.post("/getblock") 343 | def getblock(): 344 | log_ip(request, inspect.stack()[0][3]) 345 | hhash = request.forms.get("headerhash") 346 | return cached_get_block(hhash) 347 | 348 | 349 | @app.post("/checkblock") 350 | def checkblock(): 351 | log_ip(request, inspect.stack()[0][3]) 352 | headerhash = request.forms.get("headerhash") 353 | if get_block_from_db(headerhash): 354 | return json.dumps(True) 355 | return json.dumps(False) 356 | 357 | 358 | @app.post("/getblockhashes") 359 | def send_block_hashes(): 360 | log_ip(request, inspect.stack()[0][3]) 361 | peer_height = int(request.forms.get("myheight")) 362 | hash_list = [] 363 | for i in range(peer_height, BLOCKCHAIN.active_chain.length): 364 | hash_list.append(dhash(BLOCKCHAIN.active_chain.header_list[i])) 365 | return compress(json.dumps(hash_list)).decode() 366 | 367 | 368 | @lru_cache(maxsize=16) 369 | def process_new_block(request_data: bytes) -> str: 370 | global BLOCKCHAIN 371 | block_json = decompress(request_data) 372 | if block_json: 373 | try: 374 | block = Block.from_json(block_json).object() 375 | # Check if block already exists 376 | if get_block_from_db(dhash(block.header)): 377 | logger.info("Server: Received block exists, doing nothing") 378 | return "Block already Received Before" 379 | if BLOCKCHAIN.add_block(block): 380 | logger.info("Server: Received a New Valid Block, Adding to Chain") 381 | 382 | logger.debug("Server: Sending new block to peers") 383 | # Broadcast block to other peers 384 | send_to_all_peers("/newblock", request_data) 385 | 386 | # TODO Make new chain/ orphan set for Block that is not added 387 | except Exception as e: 388 | logger.error("Server: New Block: invalid block received " + str(e)) 389 | return "Invalid Block Received" 390 | 391 | # Kill Miner 392 | t = Timer(1, miner.stop_mining) 393 | t.start() 394 | return "Block Received" 395 | logger.error("Server: Invalid Block Received") 396 | return "Invalid Block" 397 | 398 | 399 | @app.post("/newblock") 400 | def received_new_block(): 401 | log_ip(request, inspect.stack()[0][3]) 402 | return process_new_block(request.body.read()) 403 | 404 | 405 | @lru_cache(maxsize=16) 406 | def process_new_transaction(request_data: bytes) -> str: 407 | global BLOCKCHAIN 408 | transaction_json = decompress(request_data) 409 | if transaction_json: 410 | try: 411 | tx = Transaction.from_json(transaction_json).object() 412 | # Add transaction to Mempool 413 | if tx not in BLOCKCHAIN.mempool: 414 | if BLOCKCHAIN.active_chain.is_transaction_valid(tx): 415 | logger.debug("Valid Transaction received, Adding to Mempool") 416 | BLOCKCHAIN.mempool.add(tx) 417 | # Broadcast block to other peers 418 | send_to_all_peers("/newtransaction", request_data) 419 | else: 420 | logger.debug("The transation is not valid, not added to Mempool") 421 | return False, "Not Valid Transaction" 422 | else: 423 | return True, "Transaction Already received" 424 | except Exception as e: 425 | logger.error("Server: New Transaction: Invalid tx received: " + str(e)) 426 | return False, "Not Valid Transaction" 427 | return True, "Done" 428 | 429 | 430 | # Transactions for all active chains 431 | @app.post("/newtransaction") 432 | def received_new_transaction(): 433 | log_ip(request, inspect.stack()[0][3]) 434 | result, message = process_new_transaction(request.body.read()) 435 | if result: 436 | response.status = 200 437 | else: 438 | response.status = 400 439 | return message 440 | 441 | 442 | question = '''What is greater than God, 443 | more evil than the devil, 444 | the poor have it, 445 | the rich need it, 446 | and if you eat it, you'll die?''' 447 | actual_answer = "nothing" 448 | 449 | @app.get("/") 450 | def home(): 451 | log_ip(request, inspect.stack()[0][3]) 452 | message = "" 453 | message_type = "info" 454 | return template("index.html", message=message, message_type=message_type, question=question) 455 | 456 | 457 | with open('uuids.json', 'r') as file: 458 | uuid_json = file.read() 459 | valid_ids = set(json.loads(uuid_json)) 460 | 461 | @app.post("/") 462 | def puzzle(): 463 | log_ip(request, inspect.stack()[0][3]) 464 | message = "" 465 | message_type = "info" 466 | 467 | uuid = request.forms.get("uuid") 468 | pubkey = request.forms.get("pubkey") 469 | amounts = [300] 470 | 471 | if uuid in valid_ids: 472 | logger.debug("Valid Answer, Rewarding " + pubkey) 473 | message = "Well Done!" 474 | if check_balance(MY_WALLET.public_key) >= sum(amounts): 475 | result = send_bounty([pubkey], amounts) 476 | if result: 477 | message = "Your reward is being sent, please wait for it to be mined!" 478 | valid_ids.remove(uuid) 479 | else: 480 | message = "Some Error Occured, Contact Admin." 481 | message_type = "warning" 482 | else: 483 | message = "Invalid Unique ID!" 484 | message_type = "danger" 485 | 486 | return template("index.html", message=message, message_type=message_type, question=question) 487 | 488 | 489 | @app.get('/about') 490 | def about(): 491 | return template("about.html") 492 | 493 | 494 | @app.get("/wallet") 495 | def wallet(): 496 | log_ip(request, inspect.stack()[0][3]) 497 | return template("wallet.html", message="", message_type="", pubkey=MY_WALLET.public_key) 498 | 499 | 500 | @app.post("/wallet") 501 | def wallet_post(): 502 | log_ip(request, inspect.stack()[0][3]) 503 | number = int(request.forms.get("number")) 504 | 505 | message = "" 506 | message_type = "info" 507 | try: 508 | receivers = [] 509 | amounts = [] 510 | total_amount = 0 511 | 512 | for i in range(0, number): 513 | receiver = str(request.forms.get("port" + str(i))) 514 | bounty = int(request.forms.get("amount" + str(i))) 515 | 516 | publickey = "" 517 | if len(receiver) < 10: 518 | wallet = get_wallet_from_db(receiver) 519 | if wallet is not None: 520 | publickey = wallet[1] 521 | else: 522 | message = "Error with the Receiver Port ID, try again." 523 | message_type = "danger" 524 | return template("wallet.html", message=message, message_type=message_type, pubkey=MY_WALLET.public_key) 525 | else: 526 | publickey = receiver 527 | total_amount += bounty 528 | receivers.append(publickey) 529 | amounts.append(bounty) 530 | if check_balance(MY_WALLET.public_key) >= total_amount: 531 | result = send_bounty(receivers, amounts) 532 | if result: 533 | message = "Your transaction is sent, please wait for it to be mined!" 534 | else: 535 | message = "Some Error Occured, Contact Admin." 536 | message_type = "warning" 537 | else: 538 | message = "You have Insufficient Balance!" 539 | message_type = "warning" 540 | return template("wallet.html", message=message, message_type=message_type, pubkey=MY_WALLET.public_key) 541 | except Exception as e: 542 | logger.error(e) 543 | message = "Some Error Occured. Please try again later." 544 | message_type = "danger" 545 | return template("wallet.html", message=message, message_type=message_type, pubkey=MY_WALLET.public_key) 546 | 547 | 548 | @app.get("/checkmybalance") 549 | def checkblance(): 550 | log_ip(request, inspect.stack()[0][3]) 551 | return str(check_balance(MY_WALLET.public_key)) 552 | 553 | 554 | @app.route("/static/", name="static") 555 | def serve_static(filename): 556 | log_ip(request, inspect.stack()[0][3]) 557 | return static_file(filename, root="static") 558 | 559 | 560 | @app.get("/favicon.ico") 561 | def get_favicon(): 562 | log_ip(request, inspect.stack()[0][3]) 563 | return static_file("favicon.ico", root="static") 564 | 565 | 566 | @app.get("/info") 567 | def sendinfo(): 568 | log_ip(request, inspect.stack()[0][3]) 569 | s = ( 570 | "No. of Blocks: " 571 | + str(BLOCKCHAIN.active_chain.length) 572 | + "
" 573 | + dhash(BLOCKCHAIN.active_chain.header_list[-1]) 574 | + "
" 575 | + "Balance " 576 | + str(check_balance(MY_WALLET.public_key)) 577 | + "
Public Key:
" 578 | + str(get_wallet_from_db(consts.MINER_SERVER_PORT)[1]) 579 | ) 580 | return s 581 | 582 | 583 | def render_block_header(hdr): 584 | html = "" 585 | 586 | html += "" 587 | html += "" 588 | 589 | html += "" 590 | html += "" 591 | 592 | html += "" 593 | html += "" 594 | 595 | html += "" 596 | html += "" 597 | 598 | html += "" 599 | html += ( 600 | "" 605 | ) 606 | 607 | # get block 608 | block = Block.from_json(get_block_from_db(dhash(hdr))).object() 609 | 610 | html += "" 611 | html += "" 612 | 613 | # for i, transaction in enumerate(block.transactions): 614 | # s = "coinbase: " + str(transaction.is_coinbase) + ", fees: " + str(transaction.fees) 615 | # html += "" 616 | 617 | html += "
" + "Height" + "" + str(hdr.height) + "
" + "Block Hash" + "" + dhash(hdr) + "
" + "Prev Block Hash" + "" + str(hdr.prev_block_hash) + "
" + "Merkle Root" + "" + str(hdr.merkle_root) + "
" + "Timestamp" + "" 601 | + str(datetime.fromtimestamp(hdr.timestamp).strftime("%d-%m-%Y %H:%M:%S")) 602 | + " (" 603 | + str(hdr.timestamp) 604 | + ")
" + "Transactions" + "" + str(len(block.transactions)) + "
Transaction " + str(i) + "" + str(s) + "
" 618 | return str(html) 619 | 620 | 621 | @app.get("/chains") 622 | def visualize_chain(): 623 | log_ip(request, inspect.stack()[0][3]) 624 | data = [] 625 | start = BLOCKCHAIN.active_chain.length - 10 if BLOCKCHAIN.active_chain.length > 10 else 0 626 | headers = [] 627 | hdr_list = BLOCKCHAIN.active_chain.header_list 628 | if len(hdr_list) > 200: 629 | hdr_list = BLOCKCHAIN.active_chain.header_list[:100] + BLOCKCHAIN.active_chain.header_list[-100:] 630 | for hdr in hdr_list: 631 | d = {} 632 | d["hash"] = dhash(hdr)[-5:] 633 | d["time"] = hdr.timestamp 634 | d["data"] = render_block_header(hdr) 635 | headers.append(d) 636 | data.append(headers) 637 | return template("chains.html", data=data, start=start) 638 | 639 | 640 | @app.get("/explorer") 641 | def explorer(): 642 | log_ip(request, inspect.stack()[0][3]) 643 | prev = int(request.query.prev or 0) 644 | if prev < 0: 645 | prev = 0 646 | hdr_list = list(reversed(BLOCKCHAIN.active_chain.header_list)) 647 | indexes = [i for i in range(prev * 8, (prev + 1) * 8) if i < len(hdr_list)] 648 | blocks = [Block.from_json(get_block_from_db(dhash(hdr_list[i]))).object() for i in indexes] 649 | transactions = list(BLOCKCHAIN.mempool) 650 | return template("explorer.html", blocks=blocks, transactions=transactions, prev=prev) 651 | 652 | 653 | @app.route("/block/", name="transaction") 654 | def block(blockhash): 655 | log_ip(request, inspect.stack()[0][3]) 656 | try: 657 | block = Block.from_json(get_block_from_db(blockhash)).object() 658 | except Exception as e: 659 | logger.debug("BLOCK/blockhash: " + str(e)) 660 | return template("error.html") 661 | return template("block.html", block=block) 662 | 663 | 664 | @app.route("/transaction//", name="transaction") 665 | def transaction(blockhash, txhash): 666 | log_ip(request, inspect.stack()[0][3]) 667 | try: 668 | block = Block.from_json(get_block_from_db(blockhash)).object() 669 | tx = None 670 | for t in block.transactions: 671 | if t.hash() == txhash: 672 | tx = t 673 | except Exception as e: 674 | logger.debug("Transaction/bhash/tx: " + str(e)) 675 | return template("error.html") 676 | return template("transaction.html", tx=tx, block=block) 677 | 678 | 679 | @app.route("/address/", name="account") 680 | def account(pubkey): 681 | log_ip(request, inspect.stack()[0][3]) 682 | balance = check_balance(pubkey) 683 | tx_hist = BLOCKCHAIN.active_chain.transaction_history.get(pubkey) 684 | return template("account.html", tx_hist=tx_hist, balance=balance, pubkey=pubkey) 685 | 686 | 687 | @app.post("/mining") 688 | def mining(): 689 | log_ip(request, inspect.stack()[0][3]) 690 | password = request.body.read().decode("utf-8") 691 | hashed = b"\x11`\x1e\xdd\xd1\xb6\x80\x0f\xd4\xb0t\x90\x9b\xd3]\xa0\xcc\x1d\x04$\x8b\xb1\x19J\xaa!T5-\x9eJ\xfcI5\xc0\xbb\xf5\xb1\x9d\xba\xbef@\xa1)\xcf\x9b]c(R\x91\x0e\x9dMM\xb6\x94\xa9\xe2\x94il\x15" 692 | dk = hashlib.pbkdf2_hmac("sha512", password.encode("utf-8"), b"forgeteverythingthatyouthinkyouknow", 200000) 693 | if hashed == dk: 694 | consts.NO_MINING = not consts.NO_MINING 695 | logger.info("Mining: " + str(not consts.NO_MINING)) 696 | return "Mining Toggled, " + "NOT MINING" if consts.NO_MINING else "MINING" 697 | else: 698 | return "Password Mismatch," + "NOT MINING" if consts.NO_MINING else "MINING" 699 | 700 | 701 | @app.route("/") 702 | @error(403) 703 | @error(404) 704 | @error(505) 705 | def error_handle(url="url", error="404"): 706 | log_ip(request, inspect.stack()[0][3]) 707 | return template("error.html") 708 | 709 | 710 | if __name__ == "__main__": 711 | try: 712 | if consts.NEW_BLOCKCHAIN: 713 | logger.info("FullNode: Starting New Chain from Genesis") 714 | BLOCKCHAIN.add_block(genesis_block) 715 | else: 716 | # Restore Blockchain 717 | logger.info("FullNode: Restoring Existing Chain") 718 | header_list = read_header_list_from_db() 719 | BLOCKCHAIN.build_from_header_list(header_list) 720 | 721 | # Sync with all my peers 722 | sync_with_peers() 723 | 724 | # Start mining Thread 725 | Thread(target=start_mining_thread, daemon=True).start() 726 | if consts.NO_MINING: 727 | logger.info("FullNode: Not Mining") 728 | 729 | # Start server 730 | if LINE_PROFILING: 731 | from wsgi_lineprof.middleware import LineProfilerMiddleware 732 | 733 | with open("lineprof" + str(consts.MINER_SERVER_PORT) + ".log", "w") as f: 734 | app = LineProfilerMiddleware(app, stream=f, async_stream=True) 735 | waitress.serve(app, host="0.0.0.0", threads=16, port=consts.MINER_SERVER_PORT) 736 | else: 737 | waitress.serve(app, host="0.0.0.0", threads=16, port=consts.MINER_SERVER_PORT) 738 | 739 | except KeyboardInterrupt: 740 | miner.stop_mining() 741 | -------------------------------------------------------------------------------- /src/static/css/vis.css: -------------------------------------------------------------------------------- 1 | .vis .overlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | 8 | /* Must be displayed above for example selected Timeline items */ 9 | z-index: 10; 10 | } 11 | 12 | .vis-active { 13 | box-shadow: 0 0 10px #86d5f8; 14 | } 15 | 16 | /* override some bootstrap styles screwing up the timelines css */ 17 | 18 | .vis [class*="span"] { 19 | min-height: 0; 20 | width: auto; 21 | } 22 | 23 | div.vis-configuration { 24 | position:relative; 25 | display:block; 26 | float:left; 27 | font-size:12px; 28 | } 29 | 30 | div.vis-configuration-wrapper { 31 | display:block; 32 | width:700px; 33 | } 34 | 35 | div.vis-configuration-wrapper::after { 36 | clear: both; 37 | content: ""; 38 | display: block; 39 | } 40 | 41 | div.vis-configuration.vis-config-option-container{ 42 | display:block; 43 | width:495px; 44 | background-color: #ffffff; 45 | border:2px solid #f7f8fa; 46 | border-radius:4px; 47 | margin-top:20px; 48 | left:10px; 49 | padding-left:5px; 50 | } 51 | 52 | div.vis-configuration.vis-config-button{ 53 | display:block; 54 | width:495px; 55 | height:25px; 56 | vertical-align: middle; 57 | line-height:25px; 58 | background-color: #f7f8fa; 59 | border:2px solid #ceced0; 60 | border-radius:4px; 61 | margin-top:20px; 62 | left:10px; 63 | padding-left:5px; 64 | cursor: pointer; 65 | margin-bottom:30px; 66 | } 67 | 68 | div.vis-configuration.vis-config-button.hover{ 69 | background-color: #4588e6; 70 | border:2px solid #214373; 71 | color:#ffffff; 72 | } 73 | 74 | div.vis-configuration.vis-config-item{ 75 | display:block; 76 | float:left; 77 | width:495px; 78 | height:25px; 79 | vertical-align: middle; 80 | line-height:25px; 81 | } 82 | 83 | 84 | div.vis-configuration.vis-config-item.vis-config-s2{ 85 | left:10px; 86 | background-color: #f7f8fa; 87 | padding-left:5px; 88 | border-radius:3px; 89 | } 90 | div.vis-configuration.vis-config-item.vis-config-s3{ 91 | left:20px; 92 | background-color: #e4e9f0; 93 | padding-left:5px; 94 | border-radius:3px; 95 | } 96 | div.vis-configuration.vis-config-item.vis-config-s4{ 97 | left:30px; 98 | background-color: #cfd8e6; 99 | padding-left:5px; 100 | border-radius:3px; 101 | } 102 | 103 | div.vis-configuration.vis-config-header{ 104 | font-size:18px; 105 | font-weight: bold; 106 | } 107 | 108 | div.vis-configuration.vis-config-label{ 109 | width:120px; 110 | height:25px; 111 | line-height: 25px; 112 | } 113 | 114 | div.vis-configuration.vis-config-label.vis-config-s3{ 115 | width:110px; 116 | } 117 | div.vis-configuration.vis-config-label.vis-config-s4{ 118 | width:100px; 119 | } 120 | 121 | div.vis-configuration.vis-config-colorBlock{ 122 | top:1px; 123 | width:30px; 124 | height:19px; 125 | border:1px solid #444444; 126 | border-radius:2px; 127 | padding:0px; 128 | margin:0px; 129 | cursor:pointer; 130 | } 131 | 132 | input.vis-configuration.vis-config-checkbox { 133 | left:-5px; 134 | } 135 | 136 | 137 | input.vis-configuration.vis-config-rangeinput{ 138 | position:relative; 139 | top:-5px; 140 | width:60px; 141 | /*height:13px;*/ 142 | padding:1px; 143 | margin:0; 144 | pointer-events:none; 145 | } 146 | 147 | input.vis-configuration.vis-config-range{ 148 | /*removes default webkit styles*/ 149 | -webkit-appearance: none; 150 | 151 | /*fix for FF unable to apply focus style bug */ 152 | border: 0px solid white; 153 | background-color:rgba(0,0,0,0); 154 | 155 | /*required for proper track sizing in FF*/ 156 | width: 300px; 157 | height:20px; 158 | } 159 | input.vis-configuration.vis-config-range::-webkit-slider-runnable-track { 160 | width: 300px; 161 | height: 5px; 162 | background: #dedede; /* Old browsers */ 163 | background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ 164 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ 165 | background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ 166 | background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ 167 | background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ 168 | background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ 169 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ 170 | 171 | border: 1px solid #999999; 172 | box-shadow: #aaaaaa 0px 0px 3px 0px; 173 | border-radius: 3px; 174 | } 175 | input.vis-configuration.vis-config-range::-webkit-slider-thumb { 176 | -webkit-appearance: none; 177 | border: 1px solid #14334b; 178 | height: 17px; 179 | width: 17px; 180 | border-radius: 50%; 181 | background: #3876c2; /* Old browsers */ 182 | background: -moz-linear-gradient(top, #3876c2 0%, #385380 100%); /* FF3.6+ */ 183 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#3876c2), color-stop(100%,#385380)); /* Chrome,Safari4+ */ 184 | background: -webkit-linear-gradient(top, #3876c2 0%,#385380 100%); /* Chrome10+,Safari5.1+ */ 185 | background: -o-linear-gradient(top, #3876c2 0%,#385380 100%); /* Opera 11.10+ */ 186 | background: -ms-linear-gradient(top, #3876c2 0%,#385380 100%); /* IE10+ */ 187 | background: linear-gradient(to bottom, #3876c2 0%,#385380 100%); /* W3C */ 188 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3876c2', endColorstr='#385380',GradientType=0 ); /* IE6-9 */ 189 | box-shadow: #111927 0px 0px 1px 0px; 190 | margin-top: -7px; 191 | } 192 | input.vis-configuration.vis-config-range:focus { 193 | outline: none; 194 | } 195 | input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track { 196 | background: #9d9d9d; /* Old browsers */ 197 | background: -moz-linear-gradient(top, #9d9d9d 0%, #c8c8c8 99%); /* FF3.6+ */ 198 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#9d9d9d), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ 199 | background: -webkit-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ 200 | background: -o-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* Opera 11.10+ */ 201 | background: -ms-linear-gradient(top, #9d9d9d 0%,#c8c8c8 99%); /* IE10+ */ 202 | background: linear-gradient(to bottom, #9d9d9d 0%,#c8c8c8 99%); /* W3C */ 203 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#9d9d9d', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ 204 | } 205 | 206 | input.vis-configuration.vis-config-range::-moz-range-track { 207 | width: 300px; 208 | height: 10px; 209 | background: #dedede; /* Old browsers */ 210 | background: -moz-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* FF3.6+ */ 211 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#dedede), color-stop(99%,#c8c8c8)); /* Chrome,Safari4+ */ 212 | background: -webkit-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* Chrome10+,Safari5.1+ */ 213 | background: -o-linear-gradient(top, #dedede 0%, #c8c8c8 99%); /* Opera 11.10+ */ 214 | background: -ms-linear-gradient(top, #dedede 0%,#c8c8c8 99%); /* IE10+ */ 215 | background: linear-gradient(to bottom, #dedede 0%,#c8c8c8 99%); /* W3C */ 216 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#dedede', endColorstr='#c8c8c8',GradientType=0 ); /* IE6-9 */ 217 | 218 | border: 1px solid #999999; 219 | box-shadow: #aaaaaa 0px 0px 3px 0px; 220 | border-radius: 3px; 221 | } 222 | input.vis-configuration.vis-config-range::-moz-range-thumb { 223 | border: none; 224 | height: 16px; 225 | width: 16px; 226 | 227 | border-radius: 50%; 228 | background: #385380; 229 | } 230 | 231 | /*hide the outline behind the border*/ 232 | input.vis-configuration.vis-config-range:-moz-focusring{ 233 | outline: 1px solid white; 234 | outline-offset: -1px; 235 | } 236 | 237 | input.vis-configuration.vis-config-range::-ms-track { 238 | width: 300px; 239 | height: 5px; 240 | 241 | /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ 242 | background: transparent; 243 | 244 | /*leave room for the larger thumb to overflow with a transparent border */ 245 | border-color: transparent; 246 | border-width: 6px 0; 247 | 248 | /*remove default tick marks*/ 249 | color: transparent; 250 | } 251 | input.vis-configuration.vis-config-range::-ms-fill-lower { 252 | background: #777; 253 | border-radius: 10px; 254 | } 255 | input.vis-configuration.vis-config-range::-ms-fill-upper { 256 | background: #ddd; 257 | border-radius: 10px; 258 | } 259 | input.vis-configuration.vis-config-range::-ms-thumb { 260 | border: none; 261 | height: 16px; 262 | width: 16px; 263 | border-radius: 50%; 264 | background: #385380; 265 | } 266 | input.vis-configuration.vis-config-range:focus::-ms-fill-lower { 267 | background: #888; 268 | } 269 | input.vis-configuration.vis-config-range:focus::-ms-fill-upper { 270 | background: #ccc; 271 | } 272 | 273 | .vis-configuration-popup { 274 | position: absolute; 275 | background: rgba(57, 76, 89, 0.85); 276 | border: 2px solid #f2faff; 277 | line-height:30px; 278 | height:30px; 279 | width:150px; 280 | text-align:center; 281 | color: #ffffff; 282 | font-size:14px; 283 | border-radius:4px; 284 | -webkit-transition: opacity 0.3s ease-in-out; 285 | -moz-transition: opacity 0.3s ease-in-out; 286 | transition: opacity 0.3s ease-in-out; 287 | } 288 | .vis-configuration-popup:after, .vis-configuration-popup:before { 289 | left: 100%; 290 | top: 50%; 291 | border: solid transparent; 292 | content: " "; 293 | height: 0; 294 | width: 0; 295 | position: absolute; 296 | pointer-events: none; 297 | } 298 | 299 | .vis-configuration-popup:after { 300 | border-color: rgba(136, 183, 213, 0); 301 | border-left-color: rgba(57, 76, 89, 0.85); 302 | border-width: 8px; 303 | margin-top: -8px; 304 | } 305 | .vis-configuration-popup:before { 306 | border-color: rgba(194, 225, 245, 0); 307 | border-left-color: #f2faff; 308 | border-width: 12px; 309 | margin-top: -12px; 310 | } 311 | div.vis-tooltip { 312 | position: absolute; 313 | visibility: hidden; 314 | padding: 5px; 315 | white-space: nowrap; 316 | 317 | font-family: verdana; 318 | font-size:14px; 319 | color:#000000; 320 | background-color: #f5f4ed; 321 | 322 | -moz-border-radius: 3px; 323 | -webkit-border-radius: 3px; 324 | border-radius: 3px; 325 | border: 1px solid #808074; 326 | 327 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2); 328 | pointer-events: none; 329 | 330 | z-index: 5; 331 | } 332 | 333 | 334 | div.vis-color-picker { 335 | position:absolute; 336 | top: 0px; 337 | left: 30px; 338 | margin-top:-140px; 339 | margin-left:30px; 340 | width:310px; 341 | height:444px; 342 | z-index: 1; 343 | padding: 10px; 344 | border-radius:15px; 345 | background-color:#ffffff; 346 | display: none; 347 | box-shadow: rgba(0,0,0,0.5) 0px 0px 10px 0px; 348 | } 349 | 350 | div.vis-color-picker div.vis-arrow { 351 | position: absolute; 352 | top:147px; 353 | left:5px; 354 | } 355 | 356 | div.vis-color-picker div.vis-arrow::after, 357 | div.vis-color-picker div.vis-arrow::before { 358 | right: 100%; 359 | top: 50%; 360 | border: solid transparent; 361 | content: " "; 362 | height: 0; 363 | width: 0; 364 | position: absolute; 365 | pointer-events: none; 366 | } 367 | 368 | div.vis-color-picker div.vis-arrow:after { 369 | border-color: rgba(255, 255, 255, 0); 370 | border-right-color: #ffffff; 371 | border-width: 30px; 372 | margin-top: -30px; 373 | } 374 | 375 | div.vis-color-picker div.vis-color { 376 | position:absolute; 377 | width: 289px; 378 | height: 289px; 379 | cursor: pointer; 380 | } 381 | 382 | 383 | 384 | div.vis-color-picker div.vis-brightness { 385 | position: absolute; 386 | top:313px; 387 | } 388 | 389 | div.vis-color-picker div.vis-opacity { 390 | position:absolute; 391 | top:350px; 392 | } 393 | 394 | div.vis-color-picker div.vis-selector { 395 | position:absolute; 396 | top:137px; 397 | left:137px; 398 | width:15px; 399 | height:15px; 400 | border-radius:15px; 401 | border:1px solid #ffffff; 402 | background: #4c4c4c; /* Old browsers */ 403 | background: -moz-linear-gradient(top, #4c4c4c 0%, #595959 12%, #666666 25%, #474747 39%, #2c2c2c 50%, #000000 51%, #111111 60%, #2b2b2b 76%, #1c1c1c 91%, #131313 100%); /* FF3.6+ */ 404 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(12%,#595959), color-stop(25%,#666666), color-stop(39%,#474747), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(60%,#111111), color-stop(76%,#2b2b2b), color-stop(91%,#1c1c1c), color-stop(100%,#131313)); /* Chrome,Safari4+ */ 405 | background: -webkit-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Chrome10+,Safari5.1+ */ 406 | background: -o-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* Opera 11.10+ */ 407 | background: -ms-linear-gradient(top, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* IE10+ */ 408 | background: linear-gradient(to bottom, #4c4c4c 0%,#595959 12%,#666666 25%,#474747 39%,#2c2c2c 50%,#000000 51%,#111111 60%,#2b2b2b 76%,#1c1c1c 91%,#131313 100%); /* W3C */ 409 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#4c4c4c', endColorstr='#131313',GradientType=0 ); /* IE6-9 */ 410 | } 411 | 412 | 413 | 414 | div.vis-color-picker div.vis-new-color { 415 | position:absolute; 416 | width:140px; 417 | height:20px; 418 | border:1px solid rgba(0,0,0,0.1); 419 | border-radius:5px; 420 | top:380px; 421 | left:159px; 422 | text-align:right; 423 | padding-right:2px; 424 | font-size:10px; 425 | color:rgba(0,0,0,0.4); 426 | vertical-align:middle; 427 | line-height:20px; 428 | 429 | } 430 | 431 | div.vis-color-picker div.vis-initial-color { 432 | position:absolute; 433 | width:140px; 434 | height:20px; 435 | border:1px solid rgba(0,0,0,0.1); 436 | border-radius:5px; 437 | top:380px; 438 | left:10px; 439 | text-align:left; 440 | padding-left:2px; 441 | font-size:10px; 442 | color:rgba(0,0,0,0.4); 443 | vertical-align:middle; 444 | line-height:20px; 445 | } 446 | 447 | div.vis-color-picker div.vis-label { 448 | position:absolute; 449 | width:300px; 450 | left:10px; 451 | } 452 | 453 | div.vis-color-picker div.vis-label.vis-brightness { 454 | top:300px; 455 | } 456 | 457 | div.vis-color-picker div.vis-label.vis-opacity { 458 | top:338px; 459 | } 460 | 461 | div.vis-color-picker div.vis-button { 462 | position:absolute; 463 | width:68px; 464 | height:25px; 465 | border-radius:10px; 466 | vertical-align: middle; 467 | text-align:center; 468 | line-height: 25px; 469 | top:410px; 470 | border:2px solid #d9d9d9; 471 | background-color: #f7f7f7; 472 | cursor:pointer; 473 | } 474 | 475 | div.vis-color-picker div.vis-button.vis-cancel { 476 | /*border:2px solid #ff4e33;*/ 477 | /*background-color: #ff7761;*/ 478 | left:5px; 479 | } 480 | div.vis-color-picker div.vis-button.vis-load { 481 | /*border:2px solid #a153e6;*/ 482 | /*background-color: #cb8dff;*/ 483 | left:82px; 484 | } 485 | div.vis-color-picker div.vis-button.vis-apply { 486 | /*border:2px solid #4588e6;*/ 487 | /*background-color: #82b6ff;*/ 488 | left:159px; 489 | } 490 | div.vis-color-picker div.vis-button.vis-save { 491 | /*border:2px solid #45e655;*/ 492 | /*background-color: #6dff7c;*/ 493 | left:236px; 494 | } 495 | 496 | 497 | div.vis-color-picker input.vis-range { 498 | width: 290px; 499 | height:20px; 500 | } 501 | 502 | /* TODO: is this redundant? 503 | div.vis-color-picker input.vis-range-brightness { 504 | width: 289px !important; 505 | } 506 | 507 | 508 | div.vis-color-picker input.vis-saturation-range { 509 | width: 289px !important; 510 | }*/ 511 | div.vis-network div.vis-manipulation { 512 | box-sizing: content-box; 513 | 514 | border-width: 0; 515 | border-bottom: 1px; 516 | border-style:solid; 517 | border-color: #d6d9d8; 518 | background: #ffffff; /* Old browsers */ 519 | background: -moz-linear-gradient(top, #ffffff 0%, #fcfcfc 48%, #fafafa 50%, #fcfcfc 100%); /* FF3.6+ */ 520 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(48%,#fcfcfc), color-stop(50%,#fafafa), color-stop(100%,#fcfcfc)); /* Chrome,Safari4+ */ 521 | background: -webkit-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Chrome10+,Safari5.1+ */ 522 | background: -o-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* Opera 11.10+ */ 523 | background: -ms-linear-gradient(top, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* IE10+ */ 524 | background: linear-gradient(to bottom, #ffffff 0%,#fcfcfc 48%,#fafafa 50%,#fcfcfc 100%); /* W3C */ 525 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#fcfcfc',GradientType=0 ); /* IE6-9 */ 526 | 527 | padding-top:4px; 528 | position: absolute; 529 | left: 0; 530 | top: 0; 531 | width: 100%; 532 | height: 28px; 533 | } 534 | 535 | div.vis-network div.vis-edit-mode { 536 | position:absolute; 537 | left: 0; 538 | top: 5px; 539 | height: 30px; 540 | } 541 | 542 | /* FIXME: shouldn't the vis-close button be a child of the vis-manipulation div? */ 543 | 544 | div.vis-network div.vis-close { 545 | position:absolute; 546 | right: 0; 547 | top: 0; 548 | width: 30px; 549 | height: 30px; 550 | 551 | background-position: 20px 3px; 552 | background-repeat: no-repeat; 553 | background-image: url("img/network/cross.png"); 554 | cursor: pointer; 555 | -webkit-touch-callout: none; 556 | -webkit-user-select: none; 557 | -khtml-user-select: none; 558 | -moz-user-select: none; 559 | -ms-user-select: none; 560 | user-select: none; 561 | } 562 | 563 | div.vis-network div.vis-close:hover { 564 | opacity: 0.6; 565 | } 566 | 567 | div.vis-network div.vis-manipulation div.vis-button, 568 | div.vis-network div.vis-edit-mode div.vis-button { 569 | float:left; 570 | font-family: verdana; 571 | font-size: 12px; 572 | -moz-border-radius: 15px; 573 | border-radius: 15px; 574 | display:inline-block; 575 | background-position: 0px 0px; 576 | background-repeat:no-repeat; 577 | height:24px; 578 | margin-left: 10px; 579 | /*vertical-align:middle;*/ 580 | cursor: pointer; 581 | padding: 0px 8px 0px 8px; 582 | -webkit-touch-callout: none; 583 | -webkit-user-select: none; 584 | -khtml-user-select: none; 585 | -moz-user-select: none; 586 | -ms-user-select: none; 587 | user-select: none; 588 | } 589 | 590 | div.vis-network div.vis-manipulation div.vis-button:hover { 591 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.20); 592 | } 593 | 594 | div.vis-network div.vis-manipulation div.vis-button:active { 595 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.50); 596 | } 597 | 598 | div.vis-network div.vis-manipulation div.vis-button.vis-back { 599 | background-image: url("img/network/backIcon.png"); 600 | } 601 | 602 | div.vis-network div.vis-manipulation div.vis-button.vis-none:hover { 603 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); 604 | cursor: default; 605 | } 606 | div.vis-network div.vis-manipulation div.vis-button.vis-none:active { 607 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.0); 608 | } 609 | div.vis-network div.vis-manipulation div.vis-button.vis-none { 610 | padding: 0; 611 | } 612 | div.vis-network div.vis-manipulation div.notification { 613 | margin: 2px; 614 | font-weight: bold; 615 | } 616 | 617 | div.vis-network div.vis-manipulation div.vis-button.vis-add { 618 | background-image: url("img/network/addNodeIcon.png"); 619 | } 620 | 621 | div.vis-network div.vis-manipulation div.vis-button.vis-edit, 622 | div.vis-network div.vis-edit-mode div.vis-button.vis-edit { 623 | background-image: url("img/network/editIcon.png"); 624 | } 625 | 626 | div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode { 627 | background-color: #fcfcfc; 628 | border: 1px solid #cccccc; 629 | } 630 | 631 | div.vis-network div.vis-manipulation div.vis-button.vis-connect { 632 | background-image: url("img/network/connectIcon.png"); 633 | } 634 | 635 | div.vis-network div.vis-manipulation div.vis-button.vis-delete { 636 | background-image: url("img/network/deleteIcon.png"); 637 | } 638 | /* top right bottom left */ 639 | div.vis-network div.vis-manipulation div.vis-label, 640 | div.vis-network div.vis-edit-mode div.vis-label { 641 | margin: 0 0 0 23px; 642 | line-height: 25px; 643 | } 644 | div.vis-network div.vis-manipulation div.vis-separator-line { 645 | float:left; 646 | display:inline-block; 647 | width:1px; 648 | height:21px; 649 | background-color: #bdbdbd; 650 | margin: 0px 7px 0 15px; /*top right bottom left*/ 651 | } 652 | 653 | /* TODO: is this redundant? 654 | div.network-navigation_wrapper { 655 | position: absolute; 656 | left: 0; 657 | top: 0; 658 | width: 100%; 659 | height: 100%; 660 | } 661 | */ 662 | 663 | div.vis-network div.vis-navigation div.vis-button { 664 | width:34px; 665 | height:34px; 666 | -moz-border-radius: 17px; 667 | border-radius: 17px; 668 | position:absolute; 669 | display:inline-block; 670 | background-position: 2px 2px; 671 | background-repeat:no-repeat; 672 | cursor: pointer; 673 | -webkit-touch-callout: none; 674 | -webkit-user-select: none; 675 | -khtml-user-select: none; 676 | -moz-user-select: none; 677 | -ms-user-select: none; 678 | user-select: none; 679 | } 680 | 681 | div.vis-network div.vis-navigation div.vis-button:hover { 682 | box-shadow: 0 0 3px 3px rgba(56, 207, 21, 0.30); 683 | } 684 | 685 | div.vis-network div.vis-navigation div.vis-button:active { 686 | box-shadow: 0 0 1px 3px rgba(56, 207, 21, 0.95); 687 | } 688 | 689 | div.vis-network div.vis-navigation div.vis-button.vis-up { 690 | background-image: url("img/network/upArrow.png"); 691 | bottom:50px; 692 | left:55px; 693 | } 694 | div.vis-network div.vis-navigation div.vis-button.vis-down { 695 | background-image: url("img/network/downArrow.png"); 696 | bottom:10px; 697 | left:55px; 698 | } 699 | div.vis-network div.vis-navigation div.vis-button.vis-left { 700 | background-image: url("img/network/leftArrow.png"); 701 | bottom:10px; 702 | left:15px; 703 | } 704 | div.vis-network div.vis-navigation div.vis-button.vis-right { 705 | background-image: url("img/network/rightArrow.png"); 706 | bottom:10px; 707 | left:95px; 708 | } 709 | div.vis-network div.vis-navigation div.vis-button.vis-zoomIn { 710 | background-image: url("img/network/plus.png"); 711 | bottom:10px; 712 | right:15px; 713 | } 714 | div.vis-network div.vis-navigation div.vis-button.vis-zoomOut { 715 | background-image: url("img/network/minus.png"); 716 | bottom:10px; 717 | right:55px; 718 | } 719 | div.vis-network div.vis-navigation div.vis-button.vis-zoomExtends { 720 | background-image: url("img/network/zoomExtends.png"); 721 | bottom:50px; 722 | right:15px; 723 | } 724 | .vis-timeline { 725 | /* 726 | -webkit-transition: height .4s ease-in-out; 727 | transition: height .4s ease-in-out; 728 | */ 729 | } 730 | 731 | .vis-panel { 732 | /* 733 | -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; 734 | transition: height .4s ease-in-out, top .4s ease-in-out; 735 | */ 736 | } 737 | 738 | .vis-axis { 739 | /* 740 | -webkit-transition: top .4s ease-in-out; 741 | transition: top .4s ease-in-out; 742 | */ 743 | } 744 | 745 | /* TODO: get animation working nicely 746 | 747 | .vis-item { 748 | -webkit-transition: top .4s ease-in-out; 749 | transition: top .4s ease-in-out; 750 | } 751 | 752 | .vis-item.line { 753 | -webkit-transition: height .4s ease-in-out, top .4s ease-in-out; 754 | transition: height .4s ease-in-out, top .4s ease-in-out; 755 | } 756 | /**/ 757 | .vis-current-time { 758 | background-color: #FF7F6E; 759 | width: 2px; 760 | z-index: 1; 761 | pointer-events: none; 762 | } 763 | 764 | .vis-rolling-mode-btn { 765 | height: 40px; 766 | width: 40px; 767 | position: absolute; 768 | top: 7px; 769 | right: 20px; 770 | border-radius: 50%; 771 | font-size: 28px; 772 | cursor: pointer; 773 | opacity: 0.8; 774 | color: white; 775 | font-weight: bold; 776 | text-align: center; 777 | background: #3876c2; 778 | } 779 | .vis-rolling-mode-btn:before { 780 | content: "\26F6"; 781 | } 782 | 783 | .vis-rolling-mode-btn:hover { 784 | opacity: 1; 785 | } 786 | .vis-custom-time { 787 | background-color: #6E94FF; 788 | width: 2px; 789 | cursor: move; 790 | z-index: 1; 791 | } 792 | 793 | .vis-panel.vis-background.vis-horizontal .vis-grid.vis-horizontal { 794 | position: absolute; 795 | width: 100%; 796 | height: 0; 797 | border-bottom: 1px solid; 798 | } 799 | 800 | .vis-panel.vis-background.vis-horizontal .vis-grid.vis-minor { 801 | border-color: #e5e5e5; 802 | } 803 | 804 | .vis-panel.vis-background.vis-horizontal .vis-grid.vis-major { 805 | border-color: #bfbfbf; 806 | } 807 | 808 | 809 | .vis-data-axis .vis-y-axis.vis-major { 810 | width: 100%; 811 | position: absolute; 812 | color: #4d4d4d; 813 | white-space: nowrap; 814 | } 815 | 816 | .vis-data-axis .vis-y-axis.vis-major.vis-measure { 817 | padding: 0; 818 | margin: 0; 819 | border: 0; 820 | visibility: hidden; 821 | width: auto; 822 | } 823 | 824 | 825 | .vis-data-axis .vis-y-axis.vis-minor { 826 | position: absolute; 827 | width: 100%; 828 | color: #bebebe; 829 | white-space: nowrap; 830 | } 831 | 832 | .vis-data-axis .vis-y-axis.vis-minor.vis-measure { 833 | padding: 0; 834 | margin: 0; 835 | border: 0; 836 | visibility: hidden; 837 | width: auto; 838 | } 839 | 840 | .vis-data-axis .vis-y-axis.vis-title { 841 | position: absolute; 842 | color: #4d4d4d; 843 | white-space: nowrap; 844 | bottom: 20px; 845 | text-align: center; 846 | } 847 | 848 | .vis-data-axis .vis-y-axis.vis-title.vis-measure { 849 | padding: 0; 850 | margin: 0; 851 | visibility: hidden; 852 | width: auto; 853 | } 854 | 855 | .vis-data-axis .vis-y-axis.vis-title.vis-left { 856 | bottom: 0; 857 | -webkit-transform-origin: left top; 858 | -moz-transform-origin: left top; 859 | -ms-transform-origin: left top; 860 | -o-transform-origin: left top; 861 | transform-origin: left bottom; 862 | -webkit-transform: rotate(-90deg); 863 | -moz-transform: rotate(-90deg); 864 | -ms-transform: rotate(-90deg); 865 | -o-transform: rotate(-90deg); 866 | transform: rotate(-90deg); 867 | } 868 | 869 | .vis-data-axis .vis-y-axis.vis-title.vis-right { 870 | bottom: 0; 871 | -webkit-transform-origin: right bottom; 872 | -moz-transform-origin: right bottom; 873 | -ms-transform-origin: right bottom; 874 | -o-transform-origin: right bottom; 875 | transform-origin: right bottom; 876 | -webkit-transform: rotate(90deg); 877 | -moz-transform: rotate(90deg); 878 | -ms-transform: rotate(90deg); 879 | -o-transform: rotate(90deg); 880 | transform: rotate(90deg); 881 | } 882 | 883 | .vis-legend { 884 | background-color: rgba(247, 252, 255, 0.65); 885 | padding: 5px; 886 | border: 1px solid #b3b3b3; 887 | box-shadow: 2px 2px 10px rgba(154, 154, 154, 0.55); 888 | } 889 | 890 | .vis-legend-text { 891 | /*font-size: 10px;*/ 892 | white-space: nowrap; 893 | display: inline-block 894 | } 895 | 896 | .vis-item { 897 | position: absolute; 898 | color: #1A1A1A; 899 | border-color: #97B0F8; 900 | border-width: 1px; 901 | background-color: #D5DDF6; 902 | display: inline-block; 903 | z-index: 1; 904 | /*overflow: hidden;*/ 905 | } 906 | 907 | .vis-item.vis-selected { 908 | border-color: #FFC200; 909 | background-color: #FFF785; 910 | 911 | /* z-index must be higher than the z-index of custom time bar and current time bar */ 912 | z-index: 2; 913 | } 914 | 915 | .vis-editable.vis-selected { 916 | cursor: move; 917 | } 918 | 919 | .vis-item.vis-point.vis-selected { 920 | background-color: #FFF785; 921 | } 922 | 923 | .vis-item.vis-box { 924 | text-align: center; 925 | border-style: solid; 926 | border-radius: 2px; 927 | } 928 | 929 | .vis-item.vis-point { 930 | background: none; 931 | } 932 | 933 | .vis-item.vis-dot { 934 | position: absolute; 935 | padding: 0; 936 | border-width: 4px; 937 | border-style: solid; 938 | border-radius: 4px; 939 | } 940 | 941 | .vis-item.vis-range { 942 | border-style: solid; 943 | border-radius: 2px; 944 | box-sizing: border-box; 945 | } 946 | 947 | .vis-item.vis-background { 948 | border: none; 949 | background-color: rgba(213, 221, 246, 0.4); 950 | box-sizing: border-box; 951 | padding: 0; 952 | margin: 0; 953 | } 954 | 955 | .vis-item .vis-item-overflow { 956 | position: relative; 957 | width: 100%; 958 | height: 100%; 959 | padding: 0; 960 | margin: 0; 961 | overflow: hidden; 962 | } 963 | 964 | .vis-item-visible-frame { 965 | white-space: nowrap; 966 | } 967 | 968 | .vis-item.vis-range .vis-item-content { 969 | position: relative; 970 | display: inline-block; 971 | } 972 | 973 | .vis-item.vis-background .vis-item-content { 974 | position: absolute; 975 | display: inline-block; 976 | } 977 | 978 | .vis-item.vis-line { 979 | padding: 0; 980 | position: absolute; 981 | width: 0; 982 | border-left-width: 1px; 983 | border-left-style: solid; 984 | } 985 | 986 | .vis-item .vis-item-content { 987 | white-space: nowrap; 988 | box-sizing: border-box; 989 | padding: 5px; 990 | } 991 | 992 | .vis-item .vis-onUpdateTime-tooltip { 993 | position: absolute; 994 | background: #4f81bd; 995 | color: white; 996 | width: 200px; 997 | text-align: center; 998 | white-space: nowrap; 999 | padding: 5px; 1000 | border-radius: 1px; 1001 | transition: 0.4s; 1002 | -o-transition: 0.4s; 1003 | -moz-transition: 0.4s; 1004 | -webkit-transition: 0.4s; 1005 | } 1006 | 1007 | .vis-item .vis-delete, .vis-item .vis-delete-rtl { 1008 | position: absolute; 1009 | top: 0px; 1010 | width: 24px; 1011 | height: 24px; 1012 | box-sizing: border-box; 1013 | padding: 0px 5px; 1014 | cursor: pointer; 1015 | 1016 | -webkit-transition: background 0.2s linear; 1017 | -moz-transition: background 0.2s linear; 1018 | -ms-transition: background 0.2s linear; 1019 | -o-transition: background 0.2s linear; 1020 | transition: background 0.2s linear; 1021 | } 1022 | 1023 | .vis-item .vis-delete { 1024 | right: -24px; 1025 | } 1026 | 1027 | .vis-item .vis-delete-rtl { 1028 | left: -24px; 1029 | } 1030 | 1031 | .vis-item .vis-delete:after, .vis-item .vis-delete-rtl:after { 1032 | content: "\00D7"; /* MULTIPLICATION SIGN */ 1033 | color: red; 1034 | font-family: arial, sans-serif; 1035 | font-size: 22px; 1036 | font-weight: bold; 1037 | 1038 | -webkit-transition: color 0.2s linear; 1039 | -moz-transition: color 0.2s linear; 1040 | -ms-transition: color 0.2s linear; 1041 | -o-transition: color 0.2s linear; 1042 | transition: color 0.2s linear; 1043 | } 1044 | 1045 | .vis-item .vis-delete:hover, .vis-item .vis-delete-rtl:hover { 1046 | background: red; 1047 | } 1048 | 1049 | .vis-item .vis-delete:hover:after, .vis-item .vis-delete-rtl:hover:after { 1050 | color: white; 1051 | } 1052 | 1053 | .vis-item .vis-drag-center { 1054 | position: absolute; 1055 | width: 100%; 1056 | height: 100%; 1057 | top: 0; 1058 | left: 0px; 1059 | cursor: move; 1060 | } 1061 | 1062 | .vis-item.vis-range .vis-drag-left { 1063 | position: absolute; 1064 | width: 24px; 1065 | max-width: 20%; 1066 | min-width: 2px; 1067 | height: 100%; 1068 | top: 0; 1069 | left: -4px; 1070 | 1071 | cursor: w-resize; 1072 | } 1073 | 1074 | .vis-item.vis-range .vis-drag-right { 1075 | position: absolute; 1076 | width: 24px; 1077 | max-width: 20%; 1078 | min-width: 2px; 1079 | height: 100%; 1080 | top: 0; 1081 | right: -4px; 1082 | 1083 | cursor: e-resize; 1084 | } 1085 | 1086 | .vis-range.vis-item.vis-readonly .vis-drag-left, 1087 | .vis-range.vis-item.vis-readonly .vis-drag-right { 1088 | cursor: auto; 1089 | } 1090 | 1091 | 1092 | .vis-itemset { 1093 | position: relative; 1094 | padding: 0; 1095 | margin: 0; 1096 | 1097 | box-sizing: border-box; 1098 | } 1099 | 1100 | .vis-itemset .vis-background, 1101 | .vis-itemset .vis-foreground { 1102 | position: absolute; 1103 | width: 100%; 1104 | height: 100%; 1105 | overflow: visible; 1106 | } 1107 | 1108 | .vis-axis { 1109 | position: absolute; 1110 | width: 100%; 1111 | height: 0; 1112 | left: 0; 1113 | z-index: 1; 1114 | } 1115 | 1116 | .vis-foreground .vis-group { 1117 | position: relative; 1118 | box-sizing: border-box; 1119 | border-bottom: 1px solid #bfbfbf; 1120 | } 1121 | 1122 | .vis-foreground .vis-group:last-child { 1123 | border-bottom: none; 1124 | } 1125 | 1126 | .vis-nesting-group { 1127 | cursor: pointer; 1128 | } 1129 | 1130 | .vis-nested-group { 1131 | background: #f5f5f5; 1132 | } 1133 | 1134 | .vis-label.vis-nesting-group.expanded:before { 1135 | content: "\25BC"; 1136 | } 1137 | 1138 | .vis-label.vis-nesting-group.collapsed-rtl:before { 1139 | content: "\25C0"; 1140 | } 1141 | 1142 | .vis-label.vis-nesting-group.collapsed:before { 1143 | content: "\25B6"; 1144 | } 1145 | 1146 | .vis-overlay { 1147 | position: absolute; 1148 | top: 0; 1149 | left: 0; 1150 | width: 100%; 1151 | height: 100%; 1152 | z-index: 10; 1153 | } 1154 | 1155 | .vis-labelset { 1156 | position: relative; 1157 | 1158 | overflow: hidden; 1159 | 1160 | box-sizing: border-box; 1161 | } 1162 | 1163 | .vis-labelset .vis-label { 1164 | position: relative; 1165 | left: 0; 1166 | top: 0; 1167 | width: 100%; 1168 | color: #4d4d4d; 1169 | 1170 | box-sizing: border-box; 1171 | } 1172 | 1173 | .vis-labelset .vis-label { 1174 | border-bottom: 1px solid #bfbfbf; 1175 | } 1176 | 1177 | .vis-labelset .vis-label.draggable { 1178 | cursor: pointer; 1179 | } 1180 | 1181 | .vis-labelset .vis-label:last-child { 1182 | border-bottom: none; 1183 | } 1184 | 1185 | .vis-labelset .vis-label .vis-inner { 1186 | display: inline-block; 1187 | padding: 5px; 1188 | } 1189 | 1190 | .vis-labelset .vis-label .vis-inner.vis-hidden { 1191 | padding: 0; 1192 | } 1193 | 1194 | .vis-panel { 1195 | position: absolute; 1196 | 1197 | padding: 0; 1198 | margin: 0; 1199 | 1200 | box-sizing: border-box; 1201 | } 1202 | 1203 | .vis-panel.vis-center, 1204 | .vis-panel.vis-left, 1205 | .vis-panel.vis-right, 1206 | .vis-panel.vis-top, 1207 | .vis-panel.vis-bottom { 1208 | border: 1px #bfbfbf; 1209 | } 1210 | 1211 | .vis-panel.vis-center, 1212 | .vis-panel.vis-left, 1213 | .vis-panel.vis-right { 1214 | border-top-style: solid; 1215 | border-bottom-style: solid; 1216 | overflow: hidden; 1217 | } 1218 | 1219 | .vis-left.vis-panel.vis-vertical-scroll, .vis-right.vis-panel.vis-vertical-scroll { 1220 | height: 100%; 1221 | overflow-x: hidden; 1222 | overflow-y: scroll; 1223 | } 1224 | 1225 | .vis-left.vis-panel.vis-vertical-scroll { 1226 | direction: rtl; 1227 | } 1228 | 1229 | .vis-left.vis-panel.vis-vertical-scroll .vis-content { 1230 | direction: ltr; 1231 | } 1232 | 1233 | .vis-right.vis-panel.vis-vertical-scroll { 1234 | direction: ltr; 1235 | } 1236 | 1237 | .vis-right.vis-panel.vis-vertical-scroll .vis-content { 1238 | direction: rtl; 1239 | } 1240 | 1241 | .vis-panel.vis-center, 1242 | .vis-panel.vis-top, 1243 | .vis-panel.vis-bottom { 1244 | border-left-style: solid; 1245 | border-right-style: solid; 1246 | } 1247 | 1248 | .vis-background { 1249 | overflow: hidden; 1250 | } 1251 | 1252 | .vis-panel > .vis-content { 1253 | position: relative; 1254 | } 1255 | 1256 | .vis-panel .vis-shadow { 1257 | position: absolute; 1258 | width: 100%; 1259 | height: 1px; 1260 | box-shadow: 0 0 10px rgba(0,0,0,0.8); 1261 | /* TODO: find a nice way to ensure vis-shadows are drawn on top of items 1262 | z-index: 1; 1263 | */ 1264 | } 1265 | 1266 | .vis-panel .vis-shadow.vis-top { 1267 | top: -1px; 1268 | left: 0; 1269 | } 1270 | 1271 | .vis-panel .vis-shadow.vis-bottom { 1272 | bottom: -1px; 1273 | left: 0; 1274 | } 1275 | .vis-graph-group0 { 1276 | fill:#4f81bd; 1277 | fill-opacity:0; 1278 | stroke-width:2px; 1279 | stroke: #4f81bd; 1280 | } 1281 | 1282 | .vis-graph-group1 { 1283 | fill:#f79646; 1284 | fill-opacity:0; 1285 | stroke-width:2px; 1286 | stroke: #f79646; 1287 | } 1288 | 1289 | .vis-graph-group2 { 1290 | fill: #8c51cf; 1291 | fill-opacity:0; 1292 | stroke-width:2px; 1293 | stroke: #8c51cf; 1294 | } 1295 | 1296 | .vis-graph-group3 { 1297 | fill: #75c841; 1298 | fill-opacity:0; 1299 | stroke-width:2px; 1300 | stroke: #75c841; 1301 | } 1302 | 1303 | .vis-graph-group4 { 1304 | fill: #ff0100; 1305 | fill-opacity:0; 1306 | stroke-width:2px; 1307 | stroke: #ff0100; 1308 | } 1309 | 1310 | .vis-graph-group5 { 1311 | fill: #37d8e6; 1312 | fill-opacity:0; 1313 | stroke-width:2px; 1314 | stroke: #37d8e6; 1315 | } 1316 | 1317 | .vis-graph-group6 { 1318 | fill: #042662; 1319 | fill-opacity:0; 1320 | stroke-width:2px; 1321 | stroke: #042662; 1322 | } 1323 | 1324 | .vis-graph-group7 { 1325 | fill:#00ff26; 1326 | fill-opacity:0; 1327 | stroke-width:2px; 1328 | stroke: #00ff26; 1329 | } 1330 | 1331 | .vis-graph-group8 { 1332 | fill:#ff00ff; 1333 | fill-opacity:0; 1334 | stroke-width:2px; 1335 | stroke: #ff00ff; 1336 | } 1337 | 1338 | .vis-graph-group9 { 1339 | fill: #8f3938; 1340 | fill-opacity:0; 1341 | stroke-width:2px; 1342 | stroke: #8f3938; 1343 | } 1344 | 1345 | .vis-timeline .vis-fill { 1346 | fill-opacity:0.1; 1347 | stroke: none; 1348 | } 1349 | 1350 | 1351 | .vis-timeline .vis-bar { 1352 | fill-opacity:0.5; 1353 | stroke-width:1px; 1354 | } 1355 | 1356 | .vis-timeline .vis-point { 1357 | stroke-width:2px; 1358 | fill-opacity:1.0; 1359 | } 1360 | 1361 | 1362 | .vis-timeline .vis-legend-background { 1363 | stroke-width:1px; 1364 | fill-opacity:0.9; 1365 | fill: #ffffff; 1366 | stroke: #c2c2c2; 1367 | } 1368 | 1369 | 1370 | .vis-timeline .vis-outline { 1371 | stroke-width:1px; 1372 | fill-opacity:1; 1373 | fill: #ffffff; 1374 | stroke: #e5e5e5; 1375 | } 1376 | 1377 | .vis-timeline .vis-icon-fill { 1378 | fill-opacity:0.3; 1379 | stroke: none; 1380 | } 1381 | 1382 | .vis-time-axis { 1383 | position: relative; 1384 | overflow: hidden; 1385 | } 1386 | 1387 | .vis-time-axis.vis-foreground { 1388 | top: 0; 1389 | left: 0; 1390 | width: 100%; 1391 | } 1392 | 1393 | .vis-time-axis.vis-background { 1394 | position: absolute; 1395 | top: 0; 1396 | left: 0; 1397 | width: 100%; 1398 | height: 100%; 1399 | } 1400 | 1401 | .vis-time-axis .vis-text { 1402 | position: absolute; 1403 | color: #4d4d4d; 1404 | padding: 3px; 1405 | overflow: hidden; 1406 | box-sizing: border-box; 1407 | 1408 | white-space: nowrap; 1409 | } 1410 | 1411 | .vis-time-axis .vis-text.vis-measure { 1412 | position: absolute; 1413 | padding-left: 0; 1414 | padding-right: 0; 1415 | margin-left: 0; 1416 | margin-right: 0; 1417 | visibility: hidden; 1418 | } 1419 | 1420 | .vis-time-axis .vis-grid.vis-vertical { 1421 | position: absolute; 1422 | border-left: 1px solid; 1423 | } 1424 | 1425 | .vis-time-axis .vis-grid.vis-vertical-rtl { 1426 | position: absolute; 1427 | border-right: 1px solid; 1428 | } 1429 | 1430 | .vis-time-axis .vis-grid.vis-minor { 1431 | border-color: #e5e5e5; 1432 | } 1433 | 1434 | .vis-time-axis .vis-grid.vis-major { 1435 | border-color: #bfbfbf; 1436 | } 1437 | 1438 | 1439 | .vis-timeline { 1440 | position: relative; 1441 | border: 1px solid #bfbfbf; 1442 | 1443 | overflow: hidden; 1444 | padding: 0; 1445 | margin: 0; 1446 | 1447 | box-sizing: border-box; 1448 | } 1449 | --------------------------------------------------------------------------------