diff --git a/Dockerfile b/Dockerfile index f0c48cb..195bdc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,10 @@ ENV BUILD_LIST git RUN apk add --update $BUILD_LIST \ && git clone https://github.com/leesander1/ABC.git /abc \ + && git checkout -b docker origin/networking \ && apk --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --update add leveldb leveldb-dev \ + && apk add --no-cache gcc \ + && apk add musl-dev \ && pip install pipenv \ && pipenv --python=python3.6 \ && pipenv install \ diff --git a/Pipfile b/Pipfile index 73980f5..5058a55 100644 --- a/Pipfile +++ b/Pipfile @@ -1,21 +1,12 @@ [[source]] +name = "pypi" url = "https://pypi.python.org/simple" verify_ssl = true -name = "pypi" [dev-packages] -[requires] - -python_version = "3.6" - - [packages] - -flask = "==0.12.2" -requests = "==2.18.4" -plyvel = "*" \ No newline at end of file diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..e440d0b --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,125 @@ +{ + "_meta": { + "hash": { + "sha256": "bf2ebb1f31508383de0ec6cf0f9686bfcd145810fac55eec1083dcd70fb91b5c" + }, + "host-environment-markers": { + "implementation_name": "cpython", + "implementation_version": "3.6.2", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "4.4.0-43-Microsoft", + "platform_system": "Linux", + "platform_version": "#1-Microsoft Wed Dec 31 14:42:53 PST 2014", + "python_full_version": "3.6.2", + "python_version": "3.6", + "sys_platform": "linux" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:244be0d93b71e93fc0a0a479862051414d0e00e16435707e5bf5000f92e04694", + "sha256:5ec74291ca1136b40f0379e1128ff80e866597e4e2c1e755739a913bbc3613c0" + ], + "version": "==2017.11.5" + }, + "chardet": { + "hashes": [ + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", + "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" + ], + "version": "==6.7" + }, + "flask": { + "hashes": [ + "sha256:0749df235e3ff61ac108f69ac178c9770caeaccad2509cb762ce1f65570a8856", + "sha256:49f44461237b69ecd901cc7ce66feea0319b9158743dd27a2899962ab214dac1" + ], + "version": "==0.12.2" + }, + "idna": { + "hashes": [ + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4", + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f" + ], + "version": "==2.6" + }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, + "pycryptodome": { + "hashes": [ + "sha256:2174fa555916b5ae8bcc7747ecfe2a4d5943b42c9dcf4878e269baaae264e85d", + "sha256:9fc97cd0f6eeec59af736b3df81e5811d836fa646b89a4325672dcaf997250b3", + "sha256:8440a35ccd52f0eab0f4ece284bd13a587d86d79bd404d8914f81eda74a66de1", + "sha256:6f64d8b63034fd9289bae4cb48aa8f7049f6b8db702c7af50cb3718821d28147", + "sha256:f0196124f83221f9c5e06a68e247019466395d35d92d4ce4482c835f75302851", + "sha256:8851b1e1d85e4fb981048c8a8a8431839103f43ea3c35f1b46bae2e41699f439", + "sha256:a9e3e3e9ab0241b0303206656a74d5cd6bd00fcad6f9ffd0ba6b8e35072f74d7", + "sha256:15ced95a00b55bb2fc22f3dddde1c8d6f270089f35c3af0e07306bc2ba1e1c4e", + "sha256:f7befe2249df41e012a3d8079ab3c7089be21969591eb77b21767fa24557a7b7", + "sha256:ec560e62258358afd7a1a3d34c8860fdf478e28c0999173f2d5c618fd2fd60d3", + "sha256:18d8dfe31bf0cb53d58694903e526be68f3cf48e6e3c6dfbbc1e7042b1693af7" + ], + "version": "==3.4.7" + }, + "requests": { + "hashes": [ + "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", + "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" + ], + "version": "==2.18.4" + }, + "urllib3": { + "hashes": [ + "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", + "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" + ], + "version": "==1.22" + }, + "werkzeug": { + "hashes": [ + "sha256:e8549c143af3ce6559699a01e26fa4174f4c591dbee0a499f3cd4c3781cdec3d", + "sha256:903a7b87b74635244548b30d30db4c8947fe64c5198f58899ddcd3a13c23bb26" + ], + "version": "==0.12.2" + } + }, + "develop": {} +} diff --git a/README.md b/README.md index 8122a9f..2163474 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,15 @@ The core functions we will discuss, explore and develop are the following: 3. Communication with other nodes 4. Proof of work algorithms 5. Transactions + + +### Docker + +```bash +$ docker build -t abc . +``` +```bash +$ docker run -it --rm -p 80:5000 abc +$ docker run -it --rm -p 80:5001 abc +$ docker run -it --rm -p 80:5002 abc +``` diff --git a/main.py b/main.py index e757515..e08138c 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,9 @@ ''' Main File ''' # from block import Block +import _thread, time from src.client import CLI +from src.network.network import start_server, seed_peers +_thread.start_new_thread(start_server, ()) +time.sleep(1) CLI().cmdloop() diff --git a/src/block/block.py b/src/block/block.py index 9b1784b..44f2045 100644 --- a/src/block/block.py +++ b/src/block/block.py @@ -16,7 +16,7 @@ class Block(object): - def __init__(self, previous_hash=None, transactions=None): + def __init__(self, previous_hash=None, transactions=None, **kwargs): """ Constructor for Block class Block Header: Key info on block (80 bytes) @@ -30,19 +30,33 @@ def __init__(self, previous_hash=None, transactions=None): merkle_root - a hash of all the hashed transactions in the merkle tree """ - - # define block header attributes - self.version = version.encode('utf8') # 4 bytes - self.previous_hash = previous_hash.encode('utf8') # 32 bytes - self.merkle_root = self.merkle_root(transactions) # 32 bytes - self.timestamp = self.block_timestamp() # 4 bytes - self.nonce = None # 4 bytes - self.target = None # 4 bytes - - # define rest of block - self.transactions = transactions # NOTE: may need to change - self.txcount = len(transactions) # 4 bytes - self.size = self.block_size() # 4 bytes + self.payload = kwargs.pop('payload', None) + if self.payload: + # define block header attributes + self.version = self.payload['header']['version'].encode('utf8') # 4 bytes + self.previous_hash = self.payload['header']['parent'].encode('utf8') # 32 bytes + self.merkle_root = self.payload['header']['merkle_root'] # 32 bytes + self.timestamp = self.payload['header']['timestamp'].encode('utf8') # 4 bytes + self.nonce = self.payload['header']['nonce'] + self.target = self.payload['header']['target'] + + # define rest of block + self.transactions = self.payload['transactions'] # NOTE: may need to change + self.txcount = self.payload['txcount'] # 4 bytes + self.size = self.payload['size'] + else: + # define block header attributes + self.version = version.encode('utf8') # 4 bytes + self.previous_hash = previous_hash.encode('utf8') # 32 bytes + self.merkle_root = self.merkle_root(transactions) # 32 bytes + self.timestamp = self.block_timestamp() # 4 bytes + self.nonce = None # 4 bytes + self.target = None # 4 bytes + + # define rest of block + self.transactions = transactions # NOTE: may need to change + self.txcount = len(transactions) # 4 bytes + self.size = self.block_size() # 4 bytes def block_size(self): # calculates the size of the block and returns instance size to value diff --git a/src/client/cli.py b/src/client/cli.py index d551ea2..a176d44 100644 --- a/src/client/cli.py +++ b/src/client/cli.py @@ -1,10 +1,12 @@ ''' The cmd line interface ''' import cmd import json +import _thread from src.client.helpers import cromulon # TODO: this module should not explicitly import persist or config. Need api fot this from src.core import mine, create_transaction, get_block, init_configuration +from src.network.network import start_server, seed_peers class CLI(cmd.Cmd, object): @@ -54,6 +56,7 @@ def do_block_info(self, arg): print(json.dumps(b, indent=4, sort_keys=True)) def do_send(self, arg): + 'Send an amount' # TODO: add exception handling for wrong input format try: args = arg.split() @@ -103,6 +106,10 @@ def preloop(self): self.wallet = self.conf.get_conf("wallet") self.peers = self.conf.get_conf("peers") + # _thread.start_new_thread(start_server, ()) # start flask server on new thread + # seed_peers() + + def postloop(self): 'Do stuff on end' # this is where we want to save all the data on exit diff --git a/src/configuration/configuration.py b/src/configuration/configuration.py index 31742c5..7a65d3d 100644 --- a/src/configuration/configuration.py +++ b/src/configuration/configuration.py @@ -56,14 +56,16 @@ def create_conf(self): 'address': hashed_address, 'amount': 0 }, + 'ip': "0.0.0.0", + 'port': "5000", 'peers': { '1': { - 'ip': "127.0.0.1", - 'port': 3390 + 'ip': "0.0.0.0", + 'port': 5001 }, '2': { - 'ip': "localhost", - 'port': 3390 + 'ip': "0.0.0.0", + 'port': 5002 } } } @@ -89,6 +91,24 @@ def update_previous_hash(self, block_hash): self.save_conf() return self.conf + def add_balance(self, new_amount): + """ + Updates the wallet amount, aka the balance, by adding new_amount + :param new_amount: amount to add + :return: None + """ + self.conf["wallet"]["amount"] = self.conf["wallet"]["amount"] + new_amount + self.save_conf() + + def subtract_balance(self, amount): + """ + Subtracts an amount from the balance + :param amount: amount to subtract + :return: None + """ + self.conf["wallet"]["amount"] = self.conf["wallet"]["amount"] - amount + self.save_conf() + def get_conf(self, key=None): """ returns the value for the matching key in the configuration diff --git a/src/core/__init__.py b/src/core/__init__.py index a0bb0da..0d166ae 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -1 +1,2 @@ -from src.core.api import mine, create_transaction, get_block, init_configuration \ No newline at end of file +from src.core.api import mine, create_transaction, get_block, init_configuration +from src.core import threader \ No newline at end of file diff --git a/src/core/api.py b/src/core/api.py index d478852..3a678e6 100644 --- a/src/core/api.py +++ b/src/core/api.py @@ -8,7 +8,7 @@ from src.configuration import Configuration from src.transaction import Transaction from src.wallet import get_public_key, get_private_key - +from src.network import network def mine(): # config object @@ -26,7 +26,16 @@ def mine(): save_block(b) conf.increment_height() conf.update_previous_hash(b.block_hash()) + # TODO: create new transmit with mined block + network.transmit(b, "block") +def verify_block(b): + # need to check txns + conf = Configuration() + bh = Block(payload=json.loads(b)) + save_block(bh) + conf.increment_height() + conf.update_previous_hash(bh.block_hash()) def create_transaction(recipient, amount): """ @@ -41,6 +50,8 @@ def create_transaction(recipient, amount): tx.add_output(recipient, amount) tx.unlock_inputs(get_private_key(), get_public_key("string")) save_verified_transaction(tx.get_transaction_id(), tx.get_data()) + network.transmit(tx.get_data(), "txn") + except ValueError as e: # Will raise if insufficient utxos are found raise ValueError("INSUFFICIENT FUNDS") diff --git a/src/core/threader.py b/src/core/threader.py new file mode 100644 index 0000000..38ce7d8 --- /dev/null +++ b/src/core/threader.py @@ -0,0 +1,31 @@ +import threading + +# class Threader(object): +# ''' +# Threading class for the application +# ''' +# +# def __init__(self): +# self.threads = [] +# +# def startBackgroundThread(self, method, args=False): +# ''' +# Start new thread +# ''' +# +# if args: +# newThread = threading.Thread(target=method, args=args) +# else: +# newThread = threading.Thread(target=method) +# +# newThread.start() +# +# self.threads.append(newThread) +# +# def waitForThreads(self, timeout=5.00): +# ''' +# Send stop signal to threads and wait for them to end +# ''' +# +# for thread in self.threads: +# thread.join(timeout) \ No newline at end of file diff --git a/src/network/__init__.py b/src/network/__init__.py new file mode 100644 index 0000000..75d0058 --- /dev/null +++ b/src/network/__init__.py @@ -0,0 +1 @@ +from src.network import network diff --git a/src/network/network.py b/src/network/network.py new file mode 100644 index 0000000..67e75de --- /dev/null +++ b/src/network/network.py @@ -0,0 +1,184 @@ +''' +Networking - Server +- handle seeding peers +- handle recieving new block +- handle updating chain on startup +- handle transmitting new block to network +- handle recieving new transaction from network +- handle transmitting new transaction to network +''' + +from flask import Flask +from flask import request +import requests +import logging, json, os +from src.configuration import Configuration +from src.persist import block, save_verified_transaction, save_unverified_transaction +from src.core import api as core +from src.transaction import Transaction + +app = Flask(__name__) +# app.logger.disabled = True +log = logging.getLogger('werkzeug') +log.setLevel(logging.ERROR) + +def start_server(): + ''' + Starts the server and prevents default start + :return: + ''' + app.run(host='0.0.0.0', port='5000', use_reloader=False, debug=False) + + +def seed_peers(): + ''' + seed peers by connecting to firebase cloud function that returns a list of peers. + :return: + ''' + conf = Configuration() + url = 'https://us-central1-abc-network.cloudfunctions.net/getpeers/' + port = conf.get_conf("port") # expecting {port: 50050} + p = {'port': port} + r = requests.get(url, params=p) # Not currently working. maybe requires POST? + return r.json() + + +def req_post(host, endpoint, port, data): + ''' + trasmit data by making a request to peers + :return: + ''' + # post request + # for each peer, make a request to the endpoint with the appropriate data + url = 'http://{0}:{2}/{1}'.format(host, endpoint, port) + payload = json.dumps(data) + headers = {'content-type': 'application/json', 'dataType': 'json'} + r = requests.post(url, data=payload, headers=headers) + # print(r.json()) + return r.json() + +def req_get(host, endpoint, port, data=None): + ''' + receive data from peer through a get request to an endpoint + :return: + ''' + # get request + url = 'http://{0}:{2}/{1}'.format(host, endpoint, port) + p = data + r = requests.get(url, params=p) + return r.json() + +def transmit(data, handler): + # send data to all peers + conf = Configuration() + peers = conf.get_conf("peers") + for n in peers: + p = peers[n] + h = req_post(p["ip"], handler, p["port"], data) # make post to peer + +def sync(): + ''' + sync with peers by checking your height against peers and getting transactions. + :return: + ''' + conf = Configuration() + peers = conf.get_conf("peers") + for n in peers: + p = peers[n] + h = req_get(p["ip"], "height", p["port"]) # make request to get peers height + while h > conf.get_conf("height"): + nb = req_get(p["ip"], "block", p["port"], conf.get_conf("last_block")) + core.verify_block(nb) + + +def verify_incoming_tnx(data): + """ + Deserializes transaction and verifies it. If so, add it to verified transaction + :param data: serialized transaction + :return: None + """ + tnx_tuple = data.items() + + tnx_payload = tnx_tuple[1] + tnx_payload["transaction_id"] = tnx_tuple[0] + + tnx = Transaction(payload=tnx_payload) + + if tnx.verify(): + save_verified_transaction(tnx.get_transaction_id(), tnx.get_data()) + else: + save_unverified_transaction(tnx.get_transaction_id(), tnx.get_data()) + +@app.route('/', methods=['GET']) +def test(): + b = block.read_block("0000fff57a5e49c38152b54edfd587c738ba33708008c995d973c2d2238179a6") + r = transmit(b, "block") + # r = req_post('localhost', 'block', '5001', b) + return "working" + +@app.route('/block', methods=['POST']) +# # recieve a new block from the network. +# # should verify the block +# # if valid stop mining, update height and transmit to peers +# # start mining new block +def h_block(): + # handle getting new block + nb = request.get_json(silent=True) + core.verify_block(nb) + return json.dumps(nb) + +@app.route('/block', methods=['GET']) +# # request for a certain block +# # contain params for last block hash +# # should return the next block +def r_block(): + bh = request.args.get("block") + b = block.find_block(bh) + return json.dumps(b) + +@app.route('/height', methods=['GET']) +# request from network for block height +# should return current block height +def r_height(): + conf = Configuration() + h = {'height': conf.get_conf("height")} + return json.dumps(h) + +@app.route('/txn', methods=['POST']) +# # recieve a new txn from the network. +# # should verify the txn +# # transmit to peers either way +# # add to verified or unverified txns list +def h_txn(): + # handle the txn + return "receive txn" + +@app.route('/txn', methods=['GET']) +# # request for updated list of txns +# # should return list of txns +def r_txn(): + try: + with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'r') as file: + data = json.load(file) + file.close() + except IOError: + with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: + data = {} + json.dump(data, file) + file.close() + return json.dumps(data) + +@app.route('/peers', methods=['GET']) +# request from network for peers +# should return peers +def r_peers(): + conf = Configuration() + peers = conf.get_conf("peers") + return json.dumps(peers) + +@app.route('/ping', methods=['GET']) +# # request to check if connection is alive +# # should ping back +def ping(): + r = {'ping': 'pong'} + return json.dumps(r) diff --git a/src/persist/block.py b/src/persist/block.py index c8973d7..1d62e90 100644 --- a/src/persist/block.py +++ b/src/persist/block.py @@ -1,7 +1,7 @@ """ Getting and Putting blocks into json files """ import json import os -from src.persist.utxo import save_utxo +from src.configuration import Configuration def read_block(block_hash): @@ -15,9 +15,20 @@ def read_block(block_hash): # file does not exist or not able to read file data = {} print(e) - return data +def find_block(parent_hash=None, block_hash=None): + # find the next block based on hash.. + if not block_hash: + conf = Configuration() + block_hash = conf.get_conf("last_block") + + b = read_block(block_hash) + if b["header"]["parent"] == parent_hash: + return b + else: + find_block(parent_hash, b["header"]["parent"]) + def save_block(b): # saves the block @@ -26,4 +37,5 @@ def save_block(b): json.dump(b.info(), file, indent=4, sort_keys=True) file.close() except IOError as e: - print(e) \ No newline at end of file + print(e) + diff --git a/src/persist/transaction.py b/src/persist/transaction.py index be75693..cc640d2 100644 --- a/src/persist/transaction.py +++ b/src/persist/transaction.py @@ -1,49 +1,49 @@ -import os, json - -def save_verified_transaction(tnx_id, tnx_data): - """ - Saves the transaction to the verified pool. - Note that it will save the transaction as a key-value pair where the id is the key and the rest is the value - :param tnx_id: transaction id - :param tnx_data: transaction data - :return: None - """ - verified_tnx = {tnx_id: tnx_data} - try: - with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'r+') as file: - data = json.load(file) - data.update(verified_tnx) - - file.seek(0) - json.dump(data, file) - file.close() - except IOError as e: - with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: - data = {} - data.update(verified_tnx) - json.dump(data, file) - file.close() - -def save_unverified_transaction(tnx_id, tnx_data): - """ - Saves the transaction to the unverified pool. - Note that it will save the transaction as a key-value pair where the id is the key and the rest is the value - :param tnx_id: transaction id - :param tnx_data: transaction data - :return: None - """ - unverified_tnx = {tnx_id: tnx_data} - try: - with open('{0}/unverified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'r+') as file: - data = json.load(file) - data.update([unverified_tnx]) - - file.seek(0) - json.dump(data, file) - file.close() - except IOError as e: - with open('{0}/unverified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: - data = {} - data.update([unverified_tnx]) - json.dump(data, file) +import os, json + +def save_verified_transaction(tnx_id, tnx_data): + """ + Saves the transaction to the verified pool. + Note that it will save the transaction as a key-value pair where the id is the key and the rest is the value + :param tnx_id: transaction id + :param tnx_data: transaction data + :return: None + """ + verified_tnx = {tnx_id: tnx_data} + try: + with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'r+') as file: + data = json.load(file) + data.update(verified_tnx) + + file.seek(0) + json.dump(data, file) + file.close() + except IOError as e: + with open('{0}/verified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: + data = {} + data.update(verified_tnx) + json.dump(data, file) + file.close() + +def save_unverified_transaction(tnx_id, tnx_data): + """ + Saves the transaction to the unverified pool. + Note that it will save the transaction as a key-value pair where the id is the key and the rest is the value + :param tnx_id: transaction id + :param tnx_data: transaction data + :return: None + """ + unverified_tnx = {tnx_id: tnx_data} + try: + with open('{0}/unverified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'r+') as file: + data = json.load(file) + data.update([unverified_tnx]) + + file.seek(0) + json.dump(data, file) + file.close() + except IOError as e: + with open('{0}/unverified_transactions.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: + data = {} + data.update([unverified_tnx]) + json.dump(data, file) file.close() \ No newline at end of file diff --git a/src/persist/utxo.py b/src/persist/utxo.py index a962fe2..1e6c1c0 100644 --- a/src/persist/utxo.py +++ b/src/persist/utxo.py @@ -1,114 +1,114 @@ -import copy -import json -import os - -_PATH_UNSPENT_TNX = '{0}/utxo.json'.format(os.path.join(os.getcwd(), r'data')) - - -def get_unspent_outputs(amount): - """ - Create a dict of unspent transaction outputs that add up - to or exceed `amount` - NOTE: This function assumes that the TX fee is included in the param amount - NOTE: This is most efficient if UTXOs are sorted in descending amount value - :param amount: the minimum amount required from the utxos - :return: a dict of utxo's if sufficient funds found, otherwise an empty dict. Also returns utxo sum - """ - - try: - with open(_PATH_UNSPENT_TNX) as file: - data = json.load(file) - file.close() - except IOError: - with open(_PATH_UNSPENT_TNX, 'w') as file: - data = {} - json.dump(data, file) - file.close() - - utxos = copy.deepcopy(data) - selected_utxos = [] - utxo_sum = 0 - - # NOTE: This is random at the moment - for key, value in utxos.items(): - if utxo_sum < amount: - selected_utxos.append({ - "transaction_id": key, - "output_index": value["index"], - "block_hash": value["block"]}) - data.pop(key) - utxo_sum = utxo_sum + value["amount"] - else: - break - - if utxo_sum >= amount: - # if there was sufficient funds, remove them from the utxo file - with open('{0}/utxo.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: - json.dump(data, file) - file.close() - else: - raise ValueError("Insufficient funds") - return selected_utxos, utxo_sum - - -def find_unspent_output(transaction_id, output_index, block_hash): - """ - Get an unspent transaction output from the block chain - :param transaction_id: id of the transaction - :param output_index: index of the output within the transaction - :param block_hash: the block hash of the transaction - :return: - - a dict representing an unspent transaction output in the form: - - { - "transaction_id": "", - "output_index": "", - "address": "", - "amount": "" - } - - """ - - try: - with open('{0}/{1}.json'.format(os.path.join(os.getcwd(), r'data'), block_hash)) as file: - data = json.load(file) - file.close() - - output = data["transactions"][transaction_id]["outputs"][output_index] - - # adding additional info to the output object(since the output structure does not include index or id) - output["transaction_id"] = transaction_id - output["output_index"] = output_index - - return output - except IOError as e: - # file does not exist or not able to read file - print(e) - except KeyError as e: - # error finding utxo - print('Transaction not found\nTXID:{0}'.format(e)) - -def save_utxo(transaction_id, output_index, block_hash, amount): - new_utxo = {"{0}".format(transaction_id): { - "amount": amount, - "index": output_index, - "block": block_hash - }} - - try: - with open(_PATH_UNSPENT_TNX, 'r+') as file: - data = json.load(file) - data.update(new_utxo) - - file.seek(0) - json.dump(data, file) - file.close() - except IOError: - with open(_PATH_UNSPENT_TNX, 'w') as file: - data = {} - - data.update(new_utxo) - json.dump(data, file) - file.close() - +import copy +import json +import os + +_PATH_UNSPENT_TNX = '{0}/utxo.json'.format(os.path.join(os.getcwd(), r'data')) + + +def get_unspent_outputs(amount): + """ + Create a dict of unspent transaction outputs that add up + to or exceed `amount` + NOTE: This function assumes that the TX fee is included in the param amount + NOTE: This is most efficient if UTXOs are sorted in descending amount value + :param amount: the minimum amount required from the utxos + :return: a dict of utxo's if sufficient funds found, otherwise an empty dict. Also returns utxo sum + """ + + try: + with open(_PATH_UNSPENT_TNX) as file: + data = json.load(file) + file.close() + except IOError: + with open(_PATH_UNSPENT_TNX, 'w') as file: + data = {} + json.dump(data, file) + file.close() + + utxos = copy.deepcopy(data) + selected_utxos = [] + utxo_sum = 0 + + # NOTE: This is random at the moment + for key, value in utxos.items(): + if utxo_sum < amount: + selected_utxos.append({ + "transaction_id": key, + "output_index": value["index"], + "block_hash": value["block"]}) + data.pop(key) + utxo_sum = utxo_sum + value["amount"] + else: + break + + if utxo_sum >= amount: + # if there was sufficient funds, remove them from the utxo file + with open('{0}/utxo.json'.format(os.path.join(os.getcwd(), r'data')), 'w') as file: + json.dump(data, file) + file.close() + else: + raise ValueError("Insufficient funds") + return selected_utxos, utxo_sum + + +def find_unspent_output(transaction_id, output_index, block_hash): + """ + Get an unspent transaction output from the block chain + :param transaction_id: id of the transaction + :param output_index: index of the output within the transaction + :param block_hash: the block hash of the transaction + :return: + + a dict representing an unspent transaction output in the form: + + { + "transaction_id": "", + "output_index": "", + "address": "", + "amount": "" + } + + """ + + try: + with open('{0}/{1}.json'.format(os.path.join(os.getcwd(), r'data'), block_hash)) as file: + data = json.load(file) + file.close() + + output = data["transactions"][transaction_id]["outputs"][output_index] + + # adding additional info to the output object(since the output structure does not include index or id) + output["transaction_id"] = transaction_id + output["output_index"] = output_index + + return output + except IOError as e: + # file does not exist or not able to read file + print(e) + except KeyError as e: + # error finding utxo + print('Transaction not found\nTXID:{0}'.format(e)) + +def save_utxo(transaction_id, output_index, block_hash, amount): + new_utxo = {"{0}".format(transaction_id): { + "amount": amount, + "index": output_index, + "block": block_hash + }} + + try: + with open(_PATH_UNSPENT_TNX, 'r+') as file: + data = json.load(file) + data.update(new_utxo) + + file.seek(0) + json.dump(data, file) + file.close() + except IOError: + with open(_PATH_UNSPENT_TNX, 'w') as file: + data = {} + + data.update(new_utxo) + json.dump(data, file) + file.close() + diff --git a/src/transaction/transaction.py b/src/transaction/transaction.py index 2f153c2..d66685a 100644 --- a/src/transaction/transaction.py +++ b/src/transaction/transaction.py @@ -236,6 +236,16 @@ def get_data(self): } return transaction + def sum_of_outputs(self): + """ + Calculates the sum of all outputs + :return: Double + """ + total = 0 + + for output in self.outputs: + total = total + output["amount"] + return total def create_coinbase_tx(reward_amount): """