diff --git a/.gitignore b/.gitignore index 09dd1da..10b5b15 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ venv/* +mine_data/* +mariadb_data/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8798a81 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM ericomeehan/httpd-mod-wsgi +RUN apt update && apt upgrade -y +RUN apt install -y libmariadb3 libmariadb-dev python3-dev python3-pip +COPY . /usr/local/apache2/htdocs +RUN pip3 install --break-system-packages -r /usr/local/apache2/htdocs/requirements.txt +EXPOSE 80 diff --git a/db/createMarketDatabase.sql b/db-old/createDatabase.sql similarity index 68% rename from db/createMarketDatabase.sql rename to db-old/createDatabase.sql index 16d7337..0230207 100644 --- a/db/createMarketDatabase.sql +++ b/db-old/createDatabase.sql @@ -1,7 +1,7 @@ CREATE DATABASE IF NOT EXISTS market; USE market; CREATE TABLE IF NOT EXISTS users( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, userId VARCHAR(36) UNIQUE NOT NULL, userName VARCHAR(64) UNIQUE NOT NULL, passwordHash VARCHAR(64) NOT NULL, @@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS users( publicKey VARCHAR(128) NOT NULL ); CREATE TABLE IF NOT EXISTS buy_orders( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, buyOrderId VARCHAR(36) UNIQUE NOT NULL, timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(), expires DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP() + INTERVAL 3 DAY, @@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS buy_orders( FOREIGN KEY (userId) REFERENCES users(userId) ); CREATE TABLE IF NOT EXISTS sell_orders( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, sellOrderId VARCHAR(36) UNIQUE NOT NULL, timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(), expires DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP() + INTERVAL 3 DAY, @@ -31,15 +31,21 @@ CREATE TABLE IF NOT EXISTS sell_orders( FOREIGN KEY (userId) REFERENCES users(userId) ); CREATE TABLE IF NOT EXISTS sell_orders_items( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, cardId VARCHAR(36) NOT NULL, FOREIGN KEY (sellOrderId) REFERENCES sell_orders(sellOrderId) NOT NULL ); CREATE TABLE IF NOT EXISTS transactions( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, transactionId VARCHAR(36) UNIQUE NOT NULL, isPending BOOLEAN NOT NULL DEFAULT true, isAbandoned BOOLEAN NOT NULL DEFAULT false, FOREIGN KEY (buyOrderId) REFERENCES buy_orders(buyOrderId) NOT NULL, FOREIGN KEY (sellOrderItemSqlId) REFERENCES sell_order_items(sqlId) NOT NULL ); +CREATE USER IF NOT EXISTS '${MARKET_DB_USER}'@'%' IDENTIFIED BY '${MARKET_DB_PASSWORD}'; +CREATE USER IF NOT EXISTS '${ORDER_MATCHER_DB_USER}'@'%' IDENTIFIED BY '${ORDER_MATCHER_DB_PASSWORD}'; +CREATE USER IF NOT EXISTS '${STATUS_CHECKER_DB_USER}'@'%' IDENTIFIED BY '${STATUS_CHECKER_DB_PASSWORD}'; +GRANT ALL PRIVILEGES ON market.* TO '${MARKET_DB_USER}'@'%'; +GRANT ALL PRIVILEGES ON market.* TO '${ORDER_MATCHER_DB_USER}'@'%'; +GRANT ALL PRIVILEGES ON market.* TO '${STATUS_CHECKER_DB_USER}'@'%'; diff --git a/db/getBuyOrders.sql b/db-old/getBuyOrders.sql similarity index 100% rename from db/getBuyOrders.sql rename to db-old/getBuyOrders.sql diff --git a/db/getDeck.sql b/db-old/getDeck.sql similarity index 100% rename from db/getDeck.sql rename to db-old/getDeck.sql diff --git a/db/getLastTransactionForCard.sql b/db-old/getLastTransactionForCard.sql similarity index 100% rename from db/getLastTransactionForCard.sql rename to db-old/getLastTransactionForCard.sql diff --git a/db/getSellOrders.sql b/db-old/getSellOrders.sql similarity index 100% rename from db/getSellOrders.sql rename to db-old/getSellOrders.sql diff --git a/db/createChainDatabase.sql b/db/createMineDatabase.sql similarity index 79% rename from db/createChainDatabase.sql rename to db/createMineDatabase.sql index 70265ba..09d30a8 100644 --- a/db/createChainDatabase.sql +++ b/db/createMineDatabase.sql @@ -1,7 +1,5 @@ -CREATE DATABASE IF NOT EXISTS chain; -USE chain; CREATE TABLE IF NOT EXISTS blocks( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, blockId VARCHAR(32) UNIQUE NOT NULL, blockHash VARCHAR(64) UNIQUE NOT NULL, previousHash VARCHAR(64) UNIQUE NOT NULL, @@ -10,13 +8,16 @@ CREATE TABLE IF NOT EXISTS blocks( nonce INT NOT NULL ); CREATE TABLE IF NOT EXISTS cards( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, + blockId VARCHAR(32) UNIQUE NOT NULL, cardId VARCHAR(32) UNIQUE NOT NULL, pageId INT NOT NULL, FOREIGN KEY (blockId) REFERENCES blocks(blockId) ); CREATE TABLE IF NOT EXISTS transactions( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, + blockId VARCHAR(32) NOT NULL, + cardId VARCHAR(32) UNIQUE NOT NULL, transactionId VARCHAR(32) UNIQUE NOT NULL, timestamp DATETIME NOT NULL, sender VARCHAR(128) NOT NULL, @@ -28,7 +29,7 @@ CREATE TABLE IF NOT EXISTS transactions( FOREIGN KEY (cardId) REFERENCES cards(cardId) ); CREATE TABLE IF NOT EXISTS peers( - sqlId INT PRIMARY KEY AUTO INCREMENT, + sqlId INT PRIMARY KEY AUTO_INCREMENT, peerId VARCHAR(32) UNIQUE NOT NULL, baseUrl VARCHAR(128) UNIQUE NOT NULL, isUp BOOLEAN NOT NULL, diff --git a/db/createPeersDatabase.sql b/db/createPeersDatabase.sql deleted file mode 100644 index be04f5b..0000000 --- a/db/createPeersDatabase.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE DATABASE IF NOT EXISTS peers; -USE peers; -CREATE TABLE IF NOT EXISTS peers diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..1928219 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + wikideck: + image: ericomeehan/wikideck:latest + build: + context: . + dockerfile: Dockerfile + environment: + ROLE: api + DATA_PATH: /data + DIFFICULTY_REQUIREMENT: 3 + MINE_DB_HOST: mariadb-mine + MINE_DB_USER: mine + MINE_DB_PASSWORD: 123abc + depends_on: + - mariadb-mine + ports: + - "8080:80" + volumes: + - ./mine_data:/data + + mariadb-mine: + image: mariadb:latest + environment: + MARIADB_ROOT_PASSWORD: abc123 + MARIADB_DATABASE: mine + MARIADB_USER: mine + MARIADB_PASSWORD: 123abc + ports: + - "3306:3306" + +volumes: + mine_data: diff --git a/requirements.txt b/requirements.txt index e2589d2..0732ec3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ +cryptography +dotenv +flask +mariadb wikipedia diff --git a/wikideck/Mine/Block.py b/wikideck/Mine/Block.py index 9219e33..744e51c 100644 --- a/wikideck/Mine/Block.py +++ b/wikideck/Mine/Block.py @@ -1,33 +1,40 @@ -from card import Card - import datetime import hashlib +import json +import uuid + +from Mine.Card import Card +from Mine.Transaction import Transaction class Block(): - def __init__(self, blockId=uuid.uuid4(), previousHash=None, + def __init__(self, blockId=uuid.uuid4(), previousHash="0", timestamp=datetime.datetime.now(datetime.timezone.utc), height=0, nonce=0, - card=Card(), transactions=[], authorPublicKey=None, data=None): - self.blockId = blockId - self.previousHash = previousHash - self.timestamp = timestamp - self.height = height - self.difficulty = difficulty - self.nonce = nonce - self.card = card - self.transactions = transactions.append( - Transaction( - cardId = self.card.id, - receiver = authorPublicKey - ) - ) + difficulty=0, card=Card(), transactions=[], data=None): if data: self.load_from_data(data) + else: + self.blockId = blockId + self.previousHash = previousHash + self.timestamp = timestamp + self.height = height + self.difficulty = difficulty + self.nonce = nonce + self.card = card + self.transactions = transactions self.update() def update(self): self.blockHash = hashlib.sha256(str(self).encode('utf-8')) - def mine(self): + def mine(self, authorPrivateKey): + self.transactions.append( + Transaction( + cardId = self.card.cardId, + sender = authorPrivateKey.public_key(), + receiver = authorPrivateKey.public_key(), + authorPrivateKey = authorPrivateKey + ) + ) while int(self.blockHash.hexdigest(), 16) > 2**(256-self.difficulty): self.nonce += 1 self.update() @@ -43,20 +50,21 @@ class Block(): def validate(self): # TODO: validate blockId is uuid # TODO: validate previousHash is sha256 hash - if not int(self.blockHash.hexdigest(), 16) > 2**(256-self.difficulty): + if not int(self.blockHash.hexdigest(), 16) <= 2**(256-self.difficulty): raise self.Invalid("Hash does not meet difficulty requirement.") - # TODO: validate timestamp is timestamp - if not self.timestamp < datetime.datetime.now(): + # TODO: validate timestamp is UTC timestamp + if not self.timestamp < datetime.datetime.now(datetime.timezone.utc): raise self.Invalid("Timestamp in the future.") - if not self.height > 0: + if not self.height >= 0: raise self.Invalid("Height less than 0.") - if not self.difficulty > 0: + if not self.difficulty >= 0: raise self.Invalid("Difficulty less than 0.") - if not self.nonce > 0: + if not self.nonce >= 0: raise self.Invalid("Nonce less than 0.") self.card.validate() for transaction in self.transactions: transaction.validate() + # TODO validate that one transaction gives the card to the author def load_from_data(self, data): self.blockId = uuid.UUID(data['blockId']) @@ -87,21 +95,24 @@ class Block(): ] self.update() - def __str__(self): - # The hash of the block is the SHA256 hash of what this method returns. - return data.dumps({ - "blockId": str(self.blockId) + def as_dict(self): + return { + "blockId": str(self.blockId), "previousHash": self.previousHash, - "timestamp": str(self.timestamp) + "timestamp": str(self.timestamp), "height": self.height, "difficulty": self.difficulty, "nonce": self.nonce, - "author": self.author, - "card": str(self.card), - "transactions": [str(each) for each in transactions] - }) + "card": self.card.as_dict(), + "transactions": [each.as_dict() for each in self.transactions] + } + + + def __str__(self): + # The hash of the block is the SHA256 hash of what this method returns. + return json.dumps(self.as_dict()) class Invalid(Exception): - def __init__(message, status_code=406): + def __init__(self, message, status_code=406): super().__init__(message) self.status_code = status_code diff --git a/wikideck/Mine/Card.py b/wikideck/Mine/Card.py index 374e580..ffb2ac9 100644 --- a/wikideck/Mine/Card.py +++ b/wikideck/Mine/Card.py @@ -1,12 +1,25 @@ import requests +import json import wikipedia import uuid class Card(): - def __init__(self, cardId=uuid.uuid4(), - pageId=wikipedia.page(wikipedia.random()).pageid, data=None): + def __init__(self, cardId=uuid.uuid4(), pageId=None, data=None): self.cardId = cardId - self.pageId = pageId + while True: + try: + self.pageId = pageId if pageId else int( + wikipedia.page( + wikipedia.random() + ).pageid + ) + except wikipedia.exceptions.PageError as e: + # TODO: why is the random function returning lower-case pages? + continue + except wikipedia.exceptions.DisambiguationError as e: + # TODO pick random disambiuation option + continue + break if data: self.load_from_data(data) @@ -21,11 +34,14 @@ class Card(): self.cardId = uuid.UUID(data['cardId']) self.pageId = data['pageId'] - def __str__(self): - return json.dumps({ + def as_dict(self): + return { "cardId": str(self.cardId), "pageId": self.pageId - }) + } + + def __str__(self): + return json.dumps(self.as_dict()) class Invalid(Exception): def __init__(self, message, status_code=406): diff --git a/wikideck/Mine/Database.py b/wikideck/Mine/Database.py index 39a0d74..3013a5d 100644 --- a/wikideck/Mine/Database.py +++ b/wikideck/Mine/Database.py @@ -1,107 +1,186 @@ +import mariadb +import os +import time + +from Mine.Block import Block +from Mine.Transaction import Transaction + class Database(): + SQL_CREATE_DATABASE = [ + """ + CREATE TABLE IF NOT EXISTS mine.blocks( + sqlId INT PRIMARY KEY AUTO_INCREMENT, + blockId VARCHAR(37) UNIQUE NOT NULL, + blockHash VARCHAR(64) UNIQUE NOT NULL, + previousHash VARCHAR(64) UNIQUE NOT NULL, + timestamp DATETIME NOT NULL, + height INT UNIQUE NOT NULL, + nonce INT NOT NULL + ); + """, + """ + CREATE TABLE IF NOT EXISTS mine.cards( + sqlId INT PRIMARY KEY AUTO_INCREMENT, + blockId VARCHAR(37) UNIQUE NOT NULL, + cardId VARCHAR(37) UNIQUE NOT NULL, + pageId INT NOT NULL, + FOREIGN KEY (blockId) REFERENCES blocks(blockId) + ); + """, + """ + CREATE TABLE IF NOT EXISTS mine.transactions( + sqlId INT PRIMARY KEY AUTO_INCREMENT, + blockId VARCHAR(37) NOT NULL, + cardId VARCHAR(37) UNIQUE NOT NULL, + transactionId VARCHAR(37) UNIQUE NOT NULL, + timestamp DATETIME NOT NULL, + sender VARCHAR(128) NOT NULL, + receiver VARCHAR(128) NOT NULL, + signature VARCHAR(128) NOT NULL, + isPending BOOLEAN NOT NULL DEFAULT true, + isAbandoned BOOLEAN NOT NULL DEFAULT false, + FOREIGN KEY (blockId) REFERENCES blocks(blockId), + FOREIGN KEY (cardId) REFERENCES cards(cardId) + ); + """, + """ + CREATE TABLE IF NOT EXISTS mine.peers( + sqlId INT PRIMARY KEY AUTO_INCREMENT, + peerId VARCHAR(37) UNIQUE NOT NULL, + baseUrl VARCHAR(128) UNIQUE NOT NULL, + isUp BOOLEAN NOT NULL, + downCount INT DEFAULT 0, + lastTry TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ); + """ + ] + + SQL_GET_BLOCK_BY_ID = """ + SELECT * + FROM blocks + WHERE blockId = '{}'; + """ + SQL_GET_LAST_BLOCK = """ - SELECT * - FROM blocks - ORDER BY timestamp DESC - LIMIT 1; - """ + SELECT * + FROM blocks + ORDER BY timestamp DESC + LIMIT 1; + """ SQL_GET_CARD = """ - SELECT * - FROM cards - WHERE cardId == {}; - """ + SELECT * + FROM cards + WHERE cardId = '{}'; + """ SQL_GET_CARD_OWNER = """ - SELECT receiver - FROM transactions - WHERE cardId == {} - AND isPending == false - ORDER BY timestamp DESC - LIMIT 1; - """ + SELECT receiver + FROM transactions + WHERE cardId = '{}' + AND isPending = False + ORDER BY timestamp DESC + LIMIT 1; + """ SQL_GET_PENDING_TRANSACTIONS = """ - SELECT * - FROM transactions - WHERE isPending == true; - """ + SELECT * + FROM transactions + WHERE isPending = True + ORDER BY timestamp ASC + LIMIT {}; + """ - SQL_GET_TRANSACTION = """ - SELECT * - FROM transactions - WHERE transactionId == {} - """ + SQL_GET_TRANSACTION_BY_ID = """ + SELECT * + FROM transactions + WHERE transactionId == '{}' + """ SQL_INSERT_BLOCK = """ - INSERT INTO blocks (blockId, blockHash, previousHash, timestamp, height, nonce) - VALUES ({}, {}, {}, {}, {}, {}); - """ + INSERT INTO blocks (blockId, blockHash, previousHash, timestamp, height, nonce) + VALUES ('{}', '{}', '{}', '{}', {}, {}); + """ SQL_INSERT_TRANSACTION = """ - INSERT INTO transactions ( - transactionId, - timestamp, - sender, - receiver, - signature, - blockId, - cardId - ) - VALUES ({}, {}, {}, {}, {}); - """ + INSERT INTO transactions ( + transactionId, + timestamp, + sender, + receiver, + signature, + blockId, + cardId + ) + VALUES ({}, {}, {}, {}, {}); + """ - def __init__(self, host, port, user, password): - self.host = host - self.port = port - self.user = user - self.password = password - self.conn = mariadb.connect( - host = self.host, - port = self.port, - user = self.user, - password = self.password + def __init__(self): + delay = 2 + while True: + try: + self.conn = mariadb.connect( + host = os.getenv('MINE_DB_HOST', "mariadb"), + port = os.getenv('MINE_DB_PORT', 3306), + user = os.getenv('MINE_DB_USER', None), + password = os.getenv('MINE_DB_PASSWORD', None), + database = 'mine' + ) + for each in self.SQL_CREATE_DATABASE: + cursor = self.conn.cursor().execute(each) + except mariadb.Error as e: + time.sleep(delay := delay**2) + continue + break + + def get_block_by_id(self, blockId): + return Block( + data=self.conn.cursor().execute( + self.SQL_GET_BLOCK_BY_ID.format(blockId) + ) ) def get_last_block(self): - return Block( - data=self.conn.cursor().execute(SQL_GET_LAST_BLOCK)[0] - ) + data=self.conn.cursor().execute(self.SQL_GET_LAST_BLOCK) + return Block(data=data) if data else None def get_card(self, cardId): return Card( - data=self.conn.cursor().execute(SQL_GET_CARD.format(cardId))[0] + data=self.conn.cursor().execute(self.SQL_GET_CARD.format(cardId)) ) def get_card_owner(self, cardId): return self.conn.cursor().execute( - SQL_GET_CARD_OWNER.format(cardId) - )[0] + self.SQL_GET_CARD_OWNER.format(cardId) + ) - def get_pending_transactions(self): + def get_pending_transactions(self, limit=32768): + pendingTransactions = self.conn.cursor().execute( + self.SQL_GET_PENDING_TRANSACTIONS.format(limit) + ) return [ Transaction( data=each - ) for each in self.conn.cursor().execute(SQL_GET_PENDING_TRANSACTIONS) - ] + ) for each in pendingTransactions + ] if pendingTransactions else [] def get_transaction(self, transactionId): return Transaction( - data=self.conn.cursor().execute(SQL_GET_TRANSACTION.format(transactionId)) + data=self.conn.cursor().execute(self.SQL_GET_TRANSACTION.format(transactionId)) ) def insert_block(self, block): - return self.conn.cursor().execute(SQL_INSERT_BLOCK.format( + self.conn.cursor().execute(self.SQL_INSERT_BLOCK.format( str(block.blockId), block.blockHash.hexdigest(), - block.previousHash.hexdigest(), + block.previousHash, str(block.timestamp), block.height, block.nonce )) def insert_transaction(self, transaction): - return self.conn.cursor().execute(SQL_INSERT_TRANSACTION.format( + return self.conn.cursor().execute(self.SQL_INSERT_TRANSACTION.format( str(transaction.transactionId), str(transaction.timestamp), transaction.sender, diff --git a/wikideck/Mine/Mine.py b/wikideck/Mine/Mine.py index 20c6364..d56a4d2 100644 --- a/wikideck/Mine/Mine.py +++ b/wikideck/Mine/Mine.py @@ -1,24 +1,75 @@ import flask import json +import os +from cryptography.hazmat.primitives.asymmetric import rsa + +from Mine.Block import Block +from Mine.Card import Card from Mine.Database import Database +from Mine.Transaction import Transaction + +BLOCK_TRANSACTION_LIMIT = int(os.getenv('BLOCK_TRANSACTION_LIMIT', 32768)) +DATA_PATH = os.getenv('DATA_PATH', '/var/lib/wikideck/blocks') +DIFFICULTY_REQUIREMENT = int(os.getenv('DIFFICULTY_REQUIREMENT', 0)) mine = flask.Blueprint("mine", __name__) db = Database() +privateKey = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) -@app.get('/') -def mine(): +def save_block(block): + with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f: + f.write(str(block)) # Blocks are json strings. This is the true block. + db.insert_block(block) + +if not db.get_last_block(): + # TODO: load blocks from files + # TODO: try to get blocks from peers + originBlock = Block( + difficulty = DIFFICULTY_REQUIREMENT, + ) + originBlock.mine(privateKey) + originBlock.validate() + save_block(originBlock) + +@mine.get('/') +def index_get(): # TODO: return a page to mine through the browser - return 200 + return "Hello world!", 200 + +### +# Retrieve blocks and block data. +# Returns a skeleton block to be mined by default. +# Queries for a specific block when given parameters. +### +@mine.get('/blocks') +def blocks_get(): + # TODO: block queries + blockId = flask.request.args.get('blockId', None) + lastBlock = db.get_last_block() + return flask.jsonify( + Block( + previousHash = lastBlock.blockHash.hexdigest(), + height = lastBlock.height + 1, + difficulty = DIFFICULTY_REQUIREMENT, + transactions = [ + Transaction(data=each) for each in db.get_pending_transactions( + BLOCK_TRANSACTION_LIMIT + ) + ] + ).as_dict() + ) ### # Submit a block to the chain. # This method calls the Block().validate() method to validate the block schema. # This method performs additional validations against the rest of the chain. ### -@app.post('/blocks') +@mine.post('/blocks') def blocks_post(): - previousBlock = get_last_block() try: newBlock = Block(data=request.get_json()) newBlock.validate() @@ -35,7 +86,7 @@ def blocks_post(): raise Block.Invalid( f"Incorrect block height - should be {previousBlock.height + 1}." ) - if newBlock.difficulty != DIFFICULTY_REQUIREMENT: + if newBlock.difficulty < DIFFICULTY_REQUIREMENT: raise Block.Invalid( f"Incorrect difficulty - should be {DIFFICULTY_REQUIREMENT}." ) @@ -73,46 +124,24 @@ def blocks_post(): raise Transaction.Invalid( f"Incorrect signature on {transaction.transactionId}." ) - db.insert_block(block) - with open(f"{DATA_PATH}/{newBlock.blockId}.json", 'w') as f: - f.write(str(newBlock)) - # TODO: update transactions + save_block(block) + for transaction in newBlock.transactions: + db.update_transactions_is_pending_false(transaction.transactionId) # TODO: update peers - return str(newBlock), 200 - except: Transaction.Invalid as e: + return flask.jsonify(block.asDict()) + except Transaction.Invalid as e: return e, e.statusCode - except: Transaction.AlreadyFulfilled as e: + except Transaction.AlreadyFulfilled as e: return e, e.statusCode - except: Card.Invalid as e: + except Card.Invalid as e: return e, e.statusCode - except: Block.Invalid as e: + except Block.Invalid as e: return e, e.statusCode -### -# Retrieve blocks and block data. -# Returns a skeleton block to be mined by default. -# Queries for a specific block when given parameters. -### -@app.get('/blocks') -def blocks_get(): - blockHash = request.args.get('blockHash', None) - height = request.args.get('height', None) - # TODO: block queries - return str( - Block( - previousHash = lastBlock.blockHash, - height = lastBlock.height + 1, - difficulty = DIFFICULTY_REQUIREMENT, - transactions = [ - Transaction(data=each) for each in db.get_pending_transactions() - ] - ) - ) - ### # Retrieve card data ### -@app.get('/cards') +@mine.get('/cards') def cards_get(): # TODO: render cards in html # TODO: query cards @@ -124,7 +153,7 @@ def cards_get(): # This method performs a number of validations on the submitted transaction and returns # a status code result. ### -@app.put('/transactions') +@mine.put('/transactions') def transactions_put(): try: newTransaction = Transaction(data=request.get_json()) @@ -151,20 +180,25 @@ def transactions_put(): ### # Retrieve a transaction. ### -@app.get('/transactions') +@mine.get('/transactions') def transactions_get(): transactionId = request.args.get('transactionId', None) return get_transaction_by_id(transactionId) if transactionId else json.dumps( db.get_pending_transactions() ) -@app.post('/peers') +@mine.post('/peers') def peers_post(): - # TODO: validate peer - # TODO: add peers to database - return 200 + try: + peer = Peer(data=request.get_json()) + peer.validate() + # TODO: add peers to database + return 200 + # TODO: error handling + except Exception as e: + print('crap!') -@app.get('/peers') +@mine.get('/peers') def peers_get(): # TODO: query peers return 200 diff --git a/wikideck/Mine/Transaction.py b/wikideck/Mine/Transaction.py index a60c69b..cab0511 100644 --- a/wikideck/Mine/Transaction.py +++ b/wikideck/Mine/Transaction.py @@ -1,11 +1,17 @@ import datetime -import data +import io +import json import uuid +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding + class Transaction(): def __init__(self, transactionId=uuid.uuid4(), timestamp=datetime.datetime.now(datetime.timezone.utc), - cardId=None, sender=None, receiver=None, signature=None, data=None): + cardId=None, sender=None, receiver=None, authorPrivateKey=None, signature=None, + data=None): self.transactionId = transactionId self.timestamp = timestamp self.cardId = cardId @@ -14,6 +20,8 @@ class Transaction(): self.signature = signature if data: self.load_from_data(data) + if authorPrivateKey: + self.sign(authorPrivateKey) def validate(self): # TODO: validate transactionId @@ -22,12 +30,48 @@ class Transaction(): # TODO: validate sender # TODO: validate receiver # TODO: validate signature - if False: - raise self.Unauthorized("Failed signature check.") + try: + self.sender.verify( + self.signature, + bytearray( + json.dumps({ + "transactionId": str(self.transactionId), + "timestamp": str(self.timestamp), + "cardId": str(self.cardId), + "receiver": self.receiver.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }).encode('utf-8') + ), + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) + except Exception as e: + raise self.InvalidSignature("Invalid signature.") - def sign(self, privateKey): - # TODO: use rsa private key to sign block - return None + def sign(self, authorPrivateKey): + self.signature = authorPrivateKey.sign( + bytearray( + json.dumps({ + "transactionId": str(self.transactionId), + "timestamp": str(self.timestamp), + "cardId": str(self.cardId), + "receiver": self.receiver.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }).encode('utf-8') + ), + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=padding.PSS.MAX_LENGTH + ), + hashes.SHA256() + ) def load_from_data(self, data): self.transactionId = uuid.UUID(data['transactionId']) @@ -36,20 +80,29 @@ class Transaction(): "%Y-%m-%d %H:%M:%S.%f%z" ) self.cardId = uuid.UUID(data['cardId']) + # TODO: load rsa keys self.sender = data['sender'] self.receiver = data['receiver'] self.signature = data['signature'] + def as_dict(self): + return { + "transactionId": str(self.transactionId), + "timestamp": str(self.timestamp), + "cardId": str(self.cardId), + "receiver": self.receiver.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + "sender": self.sender.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + "signature": self.signature.hex() + } def __str__(self): - return data.dumps({ - "transactionId": str(self.id), - "timestamp": str(self.timestamp), - "cardId": self.cardId, - "sender": self.sender, - "receiver": self.receiver, - "signature": self.signature - }) + return json.dumps(self.as_dict()) class Unauthorized(Exception): def __init__(self, message, statusCode=403): @@ -70,3 +123,8 @@ class Transaction(): def __init__(self, message, statusCode=500): super().__init__(message) self.statusCode = statusCode + + class InvalidSignature(Exception): + def __init__(self, message, statusCode=400): + super().__init__(message) + self.statusCode = statusCode diff --git a/wikideck/Mine/__pycache__/Block.cpython-311.pyc b/wikideck/Mine/__pycache__/Block.cpython-311.pyc new file mode 100644 index 0000000..f39ee83 Binary files /dev/null and b/wikideck/Mine/__pycache__/Block.cpython-311.pyc differ diff --git a/wikideck/Mine/__pycache__/Card.cpython-311.pyc b/wikideck/Mine/__pycache__/Card.cpython-311.pyc new file mode 100644 index 0000000..17865fb Binary files /dev/null and b/wikideck/Mine/__pycache__/Card.cpython-311.pyc differ diff --git a/wikideck/Mine/__pycache__/Database.cpython-311.pyc b/wikideck/Mine/__pycache__/Database.cpython-311.pyc new file mode 100644 index 0000000..28064d5 Binary files /dev/null and b/wikideck/Mine/__pycache__/Database.cpython-311.pyc differ diff --git a/wikideck/Mine/__pycache__/Mine.cpython-311.pyc b/wikideck/Mine/__pycache__/Mine.cpython-311.pyc new file mode 100644 index 0000000..cb92f5f Binary files /dev/null and b/wikideck/Mine/__pycache__/Mine.cpython-311.pyc differ diff --git a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc new file mode 100644 index 0000000..073a521 Binary files /dev/null and b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc differ diff --git a/wikideck/PeerHandler/PeerHandler.py b/wikideck/PeerHandler/PeerHandler.py new file mode 100644 index 0000000..f5443bf --- /dev/null +++ b/wikideck/PeerHandler/PeerHandler.py @@ -0,0 +1,36 @@ +### +# The PeerHandler sends new blocks and transactions to available peers. +### + +import requests + +from Mine.Block import Block +from Mine.Transaction import Transaction + +class PeerHandler(): + def __init__(self): + return + + def add_peer(self. peer): + try: + db.insert_peer(peer.peerId, peer.url) + # TODO: error handling + except Exception as e: + print('crap!') + + def propogate_block(self, block): + try: + for peer in db.get_available_peers(): + requests.post(peer.url, data=str(block)) + db.update_last_try(peer.peerId) + # TODO: error handling + except Exception as e: + print('crap!') + + def propogate_transaction(self, transaction): + try: + for peer in db.get_available_peers(): + requests.post(peer.url, data=str(transaction)) + # TODO: error handling + except Exception as e: + print('crap!') diff --git a/wikideck/__init__.py b/wikideck/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wikideck/app.py b/wikideck/app.py index df45a6a..e0546be 100644 --- a/wikideck/app.py +++ b/wikideck/app.py @@ -1,29 +1,14 @@ import dotenv +import flask import time -import os -dotenv.load_dotenv() +#from Market.Market import market +from Mine.Mine import mine -ROLE = os.getenv("ROLE", None) -INTERVAL = os.getenv("INTERVAL", 15) +app = flask.Flask(__name__) +#app.register_blueprint(market, url_prefix="/market") +app.register_blueprint(mine, url_prefix="/mine") if __name__ == "__main__": - if ROLE == "api": - from Market.Market import market - from Mine.Mine import mine - app = Flask(__name__) - app.register_blueprint(market, url_prefix="/market") - app.register_blueprint(mine, url_prefix="/mine") - app.run() - elif ROLE == "order_matcher": - from OrderMatcher.OrderMatcher import OrderMatcher - orderMatcher = OrderMatcher() - while True: - orderMatcher.match_orders() - time.sleep(INTERVAL) - elif ROLE == "status_checker": - from StatusChecker.StatusChecker import StatusChecker - statusChecker = StatusChecker() - while True: - statusChecker.check_pending_transactions() - time.sleep(INTERVAL) + dotenv.load_dotenv() + app.run() diff --git a/wikideck/client.py b/wikideck/client.py new file mode 100644 index 0000000..1479dbb --- /dev/null +++ b/wikideck/client.py @@ -0,0 +1,23 @@ +import json +import os +import requests + +from cryptography.hazmat.primitives.asymmetric import rsa + +from Mine.Block import Block + +WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/') + +privateKey = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + +newBlock = Block( + data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json() + ) +newBlock.mine(privateKey) +print(newBlock) + +r = requests.post(f"{WIKIDECK_URL}/mine/blocks", data=newBlock.as_dict()) +print(r) diff --git a/wsgi_app.py b/wsgi_app.py new file mode 100644 index 0000000..f888639 --- /dev/null +++ b/wsgi_app.py @@ -0,0 +1,4 @@ +import sys +sys.path.insert(0, '/usr/local/apache2/htdocs/wikideck') + +from app import app as application