diff --git a/db-old/createDatabase.sql b/db-old/createDatabase.sql deleted file mode 100644 index 0230207..0000000 --- a/db-old/createDatabase.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE DATABASE IF NOT EXISTS market; -USE market; -CREATE TABLE IF NOT EXISTS users( - sqlId INT PRIMARY KEY AUTO_INCREMENT, - userId VARCHAR(36) UNIQUE NOT NULL, - userName VARCHAR(64) UNIQUE NOT NULL, - passwordHash VARCHAR(64) NOT NULL, - email VARCHAR(64) UNIQUE NOT NULL, - balance FLOAT NOT NULL DEFAULT 0, - publicKey VARCHAR(128) NOT NULL -); -CREATE TABLE IF NOT EXISTS buy_orders( - 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, - pageId INT NOT NULL, - price FLOAT NOT NULL, - volume INT NOT NULL, - fee FLOAT NOT NULL DEFAULT 0, - sold INT NOT NULL DEFAULT 0, - FOREIGN KEY (userId) REFERENCES users(userId) -); -CREATE TABLE IF NOT EXISTS sell_orders( - 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, - price FLOAT NOT NULL, - fee FLOAT NOT NULL, - FOREIGN KEY (userId) REFERENCES users(userId) -); -CREATE TABLE IF NOT EXISTS sell_orders_items( - 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, - 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-old/getBuyOrders.sql b/db-old/getBuyOrders.sql deleted file mode 100644 index c2ab0e3..0000000 --- a/db-old/getBuyOrders.sql +++ /dev/null @@ -1,12 +0,0 @@ -SELECT buy_order_id -FROM buy_orders -JOIN cards -ON buy_orders.title = cards.title -WHERE title == desired_title -AND price >= desired_price -AND volume > ( - SELECT count(*) - FROM transactions - WHERE buy_order_id == buy_order_id; -) -ORDER BY price DESC; diff --git a/db-old/getDeck.sql b/db-old/getDeck.sql deleted file mode 100644 index 7ea9a40..0000000 --- a/db-old/getDeck.sql +++ /dev/null @@ -1,10 +0,0 @@ -SELECT cardId, pageId, transactionId -FROM ( - SELECT cardId, pageId, transactionId, ROW_NUMBER() OVER ( - PARTITION BY cardId ORDER BY timestamp DESC - ) AS rowNumber - FROM transactions - JOIN cards - WHERE receiver == {userId} -) AS subquery -WHERE rowNumber == 0; diff --git a/db-old/getSellOrders.sql b/db-old/getSellOrders.sql deleted file mode 100644 index 332cde6..0000000 --- a/db-old/getSellOrders.sql +++ /dev/null @@ -1,11 +0,0 @@ -SELECT sell_orders.sell_order_id, sell_order_items.card_id -FROM sell_orders -JOIN sell_order_items -ON sell_orders.sell_order_id = sell_order_items.sell_order_id -WHERE price <= desired_price -AND sell_order_items.card_id NOT IN ( - SELECT card_id - FROM transactions - WHERE sell_order_id == sell_order_id; -) -ORDER BY price ASC; diff --git a/db/createMineDatabase.sql b/db/createMineDatabase.sql deleted file mode 100644 index 09d30a8..0000000 --- a/db/createMineDatabase.sql +++ /dev/null @@ -1,38 +0,0 @@ -CREATE TABLE IF NOT EXISTS blocks( - sqlId INT PRIMARY KEY AUTO_INCREMENT, - blockId VARCHAR(32) 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 cards( - 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, - 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, - 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 peers( - sqlId INT PRIMARY KEY AUTO_INCREMENT, - peerId VARCHAR(32) 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 -); diff --git a/docker-compose.yaml b/docker-compose.yaml index 9bced9f..b1658fc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,15 +1,15 @@ version: '3.8' services: - wikideck: + wikideck-mine: image: ericomeehan/wikideck:latest build: context: . dockerfile: Dockerfile environment: - ROLE: api + ROLE: mine DATA_PATH: /tmp - DIFFICULTY_REQUIREMENT: 3 + DIFFICULTY_REQUIREMENT: 10 MINE_DB_HOST: mariadb-mine MINE_DB_USER: mine MINE_DB_PASSWORD: 123abc diff --git a/static/client.js b/static/client.js deleted file mode 100644 index e69de29..0000000 diff --git a/test/Mine/TestMine.py b/test/Mine/TestMine.py new file mode 100644 index 0000000..6bdd3cc --- /dev/null +++ b/test/Mine/TestMine.py @@ -0,0 +1,261 @@ +import json +import os +import requests +import sys +import unittest +import uuid + +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'wikideck'))) + +from Mine.Block import Block +from Mine.Transaction import Transaction + +WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/') + +class TestMine(unittest.TestCase): + def test_mine_block_send_card_mine_another_block(self): + # Create two users + privateKeyA = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + privateKeyB = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + # Mine a block with privateKeyA + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockA = Block( + data = response.json() + ) + newBlockA.mine(privateKeyA) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict()) + self.assertEqual(response.status_code, 200) + # Send card from newBlockA to privateKeyB + newTransaction = Transaction( + cardId = newBlockA.card.cardId, + sender = privateKeyA.public_key(), + receiver = privateKeyB.public_key(), + authorPrivateKey = privateKeyA + ) + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 200) + # Mine a block with privateKeyB + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockB = Block( + data = response.json() + ) + newBlockB.mine(privateKeyB) + self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash) + self.assertTrue(newBlockA.timestamp < newBlockB.timestamp) + self.assertTrue(newBlockB.height == newBlockA.height + 1) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict()) + self.assertEqual(response.status_code, 200) + # Validate decks + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyA.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 0) + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyB.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId)) + self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId) + self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId)) + self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + + def test_invalid_block_modified_transaction(self): + # Create two users + privateKeyA = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + privateKeyB = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + # Mine a block with privateKeyA + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockA = Block( + data = response.json() + ) + newBlockA.mine(privateKeyA) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict()) + self.assertEqual(response.status_code, 200) + # Send card from newBlockA to privateKeyB + newTransaction = Transaction( + cardId = newBlockA.card.cardId, + sender = privateKeyA.public_key(), + receiver = privateKeyB.public_key(), + authorPrivateKey = privateKeyA + ) + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 200) + # Mine a block with privateKeyB + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockB = Block( + data = response.json() + ) + # Modify transactions + newBlockB.transactions[0].sender = privateKeyB.public_key() + newBlockB.transactions[0].receiver = privateKeyA.public_key() + newBlockB.mine(privateKeyB) + self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash) + self.assertTrue(newBlockA.timestamp < newBlockB.timestamp) + self.assertTrue(newBlockB.height == newBlockA.height + 1) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict()) + self.assertEqual(response.status_code, 400) + # Validate decks + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyA.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId)) + self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId) + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyB.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 0) + # Mine a block with privateKeyB + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockB = Block( + data = response.json() + ) + newBlockB.mine(privateKeyB) + self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash) + self.assertTrue(newBlockA.timestamp < newBlockB.timestamp) + self.assertTrue(newBlockB.height == newBlockA.height + 1) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict()) + self.assertEqual(response.status_code, 200) + # Validate decks + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyA.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 0) + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyB.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId)) + self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId) + self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId)) + self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + + def test_invalid_transaction(self): + # Create three users + privateKeyA = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + privateKeyB = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + privateKeyC = rsa.generate_private_key( + public_exponent=65537, + key_size=4096 + ) + # Mine a block with privateKeyA + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockA = Block( + data = response.json() + ) + newBlockA.mine(privateKeyA) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict()) + self.assertEqual(response.status_code, 200) + # Send card from newBlockA to privateKeyB + newTransaction = Transaction( + cardId = newBlockA.card.cardId, + sender = privateKeyA.public_key(), + receiver = privateKeyB.public_key(), + authorPrivateKey = privateKeyA + ) + # Invalidate transaction + newTransaction.cardId = uuid.uuid4() + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 400) + newTransaction.cardId = newBlockA.card.cardId + newTransaction.sender = privateKeyC.public_key() + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 400) + newTransaction.sender = privateKeyA.public_key() + newTransaction.receiver = privateKeyC.public_key() + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 400) + newTransaction.receiver = privateKeyB.public_key() + newTransaction.sign(privateKeyC) + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 400) + newTransaction.sign(privateKeyA) + response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict()) + self.assertEqual(response.status_code, 200) + # Mine a block with privateKeyB + response = requests.get(f"{WIKIDECK_URL}/blocks") + self.assertEqual(response.status_code, 200) + newBlockB = Block( + data = response.json() + ) + newBlockB.mine(privateKeyB) + self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash) + self.assertTrue(newBlockA.timestamp < newBlockB.timestamp) + self.assertTrue(newBlockB.height == newBlockA.height + 1) + response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict()) + self.assertEqual(response.status_code, 200) + # Validate decks + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyA.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 0) + response = requests.get(f"{WIKIDECK_URL}/cards", params={ + 'publicKey': privateKeyB.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + }) + self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId)) + self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId) + self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId)) + self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.json()), 2) + +if __name__ == '__main__': + unittest.main() diff --git a/db-old/getLastTransactionForCard.sql b/test/__init__.py similarity index 100% rename from db-old/getLastTransactionForCard.sql rename to test/__init__.py diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..b285e77 --- /dev/null +++ b/test/test.py @@ -0,0 +1,10 @@ +import os +import sys +import unittest + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from Mine.TestMine import TestMine + +if __name__ == '__main__': + unittest.main() diff --git a/wikideck/Mine/Block.py b/wikideck/Mine/Block.py index 2e7536e..b0b63f2 100644 --- a/wikideck/Mine/Block.py +++ b/wikideck/Mine/Block.py @@ -14,6 +14,7 @@ class Block(): if data: self.load_from_data(data) else: + delay = 2 self.blockId = blockId if blockId else uuid.uuid4() self.previousHash = previousHash self.timestamp = timestamp if timestamp else datetime.datetime.now( @@ -22,7 +23,13 @@ class Block(): self.height = height self.difficulty = difficulty self.nonce = nonce - self.card = card if card else Card() + while True: + try: + self.card = card if card else Card() + except ReadTimeout as e: + time.sleep(delay := delay**2) + continue + break self.transactions = transactions self.update() @@ -51,19 +58,40 @@ class Block(): # respective schemas. ### def validate(self): - # TODO: validate blockId is uuid - # TODO: validate previousHash is sha256 hash + if not isinstance(self.blockId, uuid.UUID): + raise TypeError(f"Block ID should be a UUID not {type(self.blockId).__name__}.") + if not self.blockId.version == 4: + raise self.Invalid(f"Block ID version should be 4 not {self.blockId.version}.") + if not isinstance(self.previousHash, str): + raise TypeError( + f"Previous hash should be string not {type(self.previousHash).__name__}." + ) if not int(self.blockHash.hexdigest(), 16) <= 2**(256-self.difficulty): raise self.Invalid("Hash does not meet difficulty requirement.") - # TODO: validate timestamp is UTC timestamp + if not isinstance(self.timestamp, datetime.datetime): + raise TypeError( + f"Timestamp should be a datetime not {type(self.timestamp).__name__}" + ) + if not self.timestamp.tzinfo == datetime.timezone.utc: + raise self.Invalid( + f"Timestamp timezone should be in UTC not {self.timestamp.tzinfo}." + ) if not self.timestamp < datetime.datetime.now(datetime.timezone.utc): - raise self.Invalid("Timestamp in the future.") + raise self.Invalid(f"Timestamp {self.timestamp} in the future.") + if not isinstance(self.height, int): + raise TypeError(f"Height should be integer not {type(self.height).__name__}.") if not self.height >= 0: - raise self.Invalid("Height less than 0.") + raise self.Invalid(f"Height {self.height} less than 0.") + if not isinstance(self.difficulty, int): + raise TypeError( + f"Difficulty should be an integer not {type(self.difficulty).__name__}." + ) if not self.difficulty >= 0: - raise self.Invalid("Difficulty less than 0.") + raise self.Invalid(f"Difficulty {self.difficulty} less than 0.") + if not isinstance(self.nonce, int): + raise TypeError(f"Nonce should be an integer not {type(self.nonce).__name__}.") if not self.nonce >= 0: - raise self.Invalid("Nonce less than 0.") + raise self.Invalid(f"Nonce {self.nonce} less than 0.") self.card.validate() seenTransactions = [] for transaction in self.transactions: diff --git a/wikideck/Mine/Card.py b/wikideck/Mine/Card.py index e0baee0..2306629 100644 --- a/wikideck/Mine/Card.py +++ b/wikideck/Mine/Card.py @@ -30,11 +30,17 @@ class Card(): self.load_from_data(data) def validate(self): + if not isinstance(self.cardId, uuid.UUID): + raise TypeError(f"Card ID should be a UUID not {type(self.cardId).__name__}.") + if not self.cardId.version == 4: + raise self.Invalid(f"Card ID version should be 4 not {self.cardId.version}.") + if not isinstance(self.pageId, int): + raise TypeError(f"Page ID should be an integer not {type(self.pageId).__name__}.") try: - # TODO: cardId is UUID wikipedia.page(pageid=self.pageId) + # TODO: may need more precision here. except Exception as e: - raise self.Invalid("Page ID does not match a Wikipedia page.") + raise self.Invalid(f"Page ID {self.pageId} does not match a Wikipedia page.") def load_from_data(self, data): self.cardId = uuid.UUID(data['cardId']) diff --git a/wikideck/Mine/Database.py b/wikideck/Mine/Database.py index d6a41e9..3064f6b 100644 --- a/wikideck/Mine/Database.py +++ b/wikideck/Mine/Database.py @@ -105,6 +105,12 @@ class Database(): VALUES (?, ?, ?, ?, ?, ?, ?); """ + SQL_GET_BLOCKS = """ + SELECT blockId, previousHash, timestamp, height, difficulty, nonce + FROM mine.blocks + ORDER BY height ASC; + """ + SQL_GET_LAST_BLOCK = """ SELECT blockId, previousHash, timestamp, height, difficulty, nonce FROM mine.blocks @@ -157,7 +163,21 @@ class Database(): SELECT transactionId, timestamp, cardId, sender, receiver, signature FROM mine.transactions WHERE blockId = ? - ORDER BY timestamp DESC; + ORDER BY timestamp ASC; + """ + + SQL_GET_DECK = """ + SELECT cards.cardId, cards.pageId + FROM ( + SELECT transactionId, cardId, timestamp, receiver, ROW_NUMBER() OVER ( + PARTITION BY transactions.cardId + ORDER BY transactions.timestamp DESC + ) AS rownumber + FROM transactions + ) AS subquery + JOIN cards ON cards.cardId = subquery.cardId + WHERE subquery.rownumber = 1 + AND receiver = ?; """ def __init__(self): @@ -197,7 +217,7 @@ class Database(): transactions = blockTransactions ) else: - return False + return None def get_card_by_id(self, cardId): cur = self.conn.cursor() @@ -206,7 +226,7 @@ class Database(): return Card( cardId = uuid.UUID(card[0]), pageId = card[1] - ) + ) if card else None def get_card_by_block_id(self, blockId): cur = self.conn.cursor() @@ -215,7 +235,32 @@ class Database(): return Card( cardId = uuid.UUID(card[0]), pageId = card[1] - ) + ) if card else None + + def get_blocks(self): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_LAST_BLOCK) + blocks = cur.fetchall() + if blocks: + blockCard = self.get_card_by_block_id(lastBlock[0]) + blockTransactions = self.get_transactions_by_block_id(lastBlock[0]) + return [ + Block( + blockId = uuid.UUID(block[0]), + previousHash = block[1], + timestamp = datetime.datetime.strptime( + block[2], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + height = block[3], + difficulty = block[4], + nonce = block[5], + card = blockCard, + transactions = blockTransactions + ) + ] + else: + return None def get_last_block(self): cur = self.conn.cursor() @@ -395,3 +440,17 @@ class Database(): transaction.signature.hex() )) + def get_deck(self, publicKey): + cur = self.conn.cursor() + cur.execute( + self.SQL_GET_DECK, + [publicKey.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8')] + ) + deck = cur.fetchall() + return [Card( + cardId = card[0], + pageId = card[1] + ) for card in deck] if deck else [] diff --git a/wikideck/Mine/Mine.py b/wikideck/Mine/Mine.py index 3afe1a7..e5e5825 100644 --- a/wikideck/Mine/Mine.py +++ b/wikideck/Mine/Mine.py @@ -4,6 +4,8 @@ import json import os import uuid +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa from Mine.Block import Block @@ -11,7 +13,7 @@ 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)) +BLOCK_TRANSACTION_LIMIT = int(os.getenv('BLOCK_TRANSACTION_LIMIT', 1024)) DATA_PATH = os.getenv('DATA_PATH', '/var/lib/wikideck/blocks') DIFFICULTY_REQUIREMENT = int(os.getenv('DIFFICULTY_REQUIREMENT', 0)) @@ -23,16 +25,16 @@ privateKey = rsa.generate_private_key( ) def save_block(block): - # Blocks are json strings. This is the true block. - with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f: - f.write(str(block)) # TODO: error handling (don't reveal mariadb errors) + # TODO: disable autocommit db.insert_block(block) db.insert_card(block.blockId, block.card) for transaction in block.transactions: db.delete_pending_transaction(transaction.transactionId) db.insert_transaction(block.blockId, transaction) - # TODO: delete block and card if inserts fail + # Blocks are json strings. This is the true block. + with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f: + f.write(str(block)) # TODO update peers def generate_origin_block(): @@ -47,8 +49,14 @@ def generate_origin_block(): @mine.get('/') def index_get(): - # TODO: return a page to mine through the browser - return "Hello world!", 200 + try: + return jsonify([each.as_dict() for each in db.get_blocks()]) + except Exception as e: + return flask.jsonify( + {'Error': str(e)} + ), e.statusCode if hasattr( + e, 'statusCode' + ) else 500 ### # Retrieve blocks and block data. @@ -86,7 +94,7 @@ def blocks_post(): previousBlock = db.get_last_block() if newBlock.previousHash != previousBlock.blockHash.hexdigest(): raise Block.Invalid( - f"Incorrect previous hash - should be {previousBlock.blockHash.hexdigest()}." + f"Previous hash should be {previousBlock.blockHash.hexdigest()}." ) if newBlock.timestamp <= previousBlock.timestamp: raise Block.Invalid( @@ -94,11 +102,11 @@ def blocks_post(): ) if newBlock.height != previousBlock.height + 1: raise Block.Invalid( - f"Incorrect block height - should be {previousBlock.height + 1}." + f"Height should be {previousBlock.height + 1} not {newBlock.height}." ) if newBlock.difficulty < DIFFICULTY_REQUIREMENT: raise Block.Invalid( - f"Incorrect difficulty - should be {DIFFICULTY_REQUIREMENT}." + f"Difficulty should be {DIFFICULTY_REQUIREMENT} not {newBlock.difficulty}." ) if len(newBlock.transactions) == 0: raise Block.Invalid( @@ -151,10 +159,25 @@ def blocks_post(): ### @mine.get('/cards') def cards_get(): - # TODO: render cards in html - # TODO: query cards - # TODO: get decks - return 200 + try: + # TODO: render cards in html + # TODO: query cards + publicKeyString = flask.request.args.get('publicKey', None) + if publicKeyString: + deck = db.get_deck( + serialization.load_pem_public_key( + publicKeyString.encode('utf-8'), + backend=default_backend() + ) + ) + return flask.jsonify([card.as_dict() for card in deck] if deck else []) + except Exception as e: + return flask.jsonify( + {'Error': str(e)} + ), e.statusCode if hasattr( + e, 'statusCode' + ) else 500 + ### # Submit a transaction to be mined in a block. diff --git a/wikideck/Mine/Transaction.py b/wikideck/Mine/Transaction.py index c5fbc12..3986bb9 100644 --- a/wikideck/Mine/Transaction.py +++ b/wikideck/Mine/Transaction.py @@ -6,7 +6,7 @@ import uuid from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric import padding, rsa class Transaction(): def __init__(self, transactionId=None, timestamp=None, cardId=None, sender=None, @@ -25,11 +25,45 @@ class Transaction(): self.sign(authorPrivateKey) def validate(self): - # TODO: validate transactionId - # TODO: validate timestamp - # TODO: validate cardId - # TODO: validate sender - # TODO: validate receiver + # TODO: better error codes + if not isinstance(self.transactionId, uuid.UUID): + raise self.Invalid( + f"Transaction ID should be a UUID not {type(self.transactionId).__name__}." + ) + if not self.transactionId.version == 4: + raise self.Invalid( + f"Transaction ID version should be 4 not {self.transactionId.version}." + ) + if not isinstance(self.timestamp, datetime.datetime): + raise self.Invalid( + f"Timestamp should be a datetime not {type(self.timestamp).__name__}" + ) + if not self.timestamp.tzinfo == datetime.timezone.utc: + raise self.Invalid( + f"Timestamp timezone should be in UTC not {self.timestamp.tzinfo}." + ) + if not self.timestamp < datetime.datetime.now(datetime.timezone.utc): + raise self.Invalid(f"Timestamp {self.timestamp} in the future.") + if not isinstance(self.cardId, uuid.UUID): + raise self.Invalid( + f"Card ID should be a UUID not {type(self.cardId).__name__}." + ) + if not self.cardId.version == 4: + raise self.Invalid( + f"Card ID version should be 4 not {self.cardId.version}." + ) + if not isinstance(self.sender, rsa.RSAPublicKey): + raise self.Invalid( + f"Sender should be an RSA public key not {type(self.sender).__name__}." + ) + if not isinstance(self.receiver, rsa.RSAPublicKey): + raise self.Invalid( + f"Receiver should be an RSA public key not {type(self.receiver).__name__}." + ) + if not isinstance(self.signature, bytes): + raise self.Invalid( + f"Signature should be bytes not {type(self.signature).__name__}." + ) try: self.sender.verify( self.signature, @@ -38,6 +72,10 @@ class Transaction(): "transactionId": str(self.transactionId), "timestamp": str(self.timestamp), "cardId": str(self.cardId), + "sender": self.sender.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), "receiver": self.receiver.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo @@ -50,6 +88,7 @@ class Transaction(): ), hashes.SHA256() ) + # TODO: may need more precision. except Exception as e: raise self.InvalidSignature(str(e)) @@ -60,6 +99,10 @@ class Transaction(): "transactionId": str(self.transactionId), "timestamp": str(self.timestamp), "cardId": str(self.cardId), + "sender": self.sender.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), "receiver": self.receiver.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo @@ -80,7 +123,7 @@ class Transaction(): "%Y-%m-%d %H:%M:%S.%f%z" ) self.cardId = uuid.UUID(data['cardId']) - # TODO: why is this sometimes a tuple? + # TODO: why is this a tuple when coming from POST /transactions? self.sender = serialization.load_pem_public_key( data['sender'].encode('utf-8'), backend=default_backend() @@ -132,12 +175,6 @@ class Transaction(): self.message = message self.statusCode = statusCode - class Abandoned(Exception): - def __init__(self, message, statusCode=500): - super().__init__(message) - self.message = message - self.statusCode = statusCode - class InvalidSignature(Exception): def __init__(self, message, statusCode=400): super().__init__(message) diff --git a/wikideck/Mine/__pycache__/Block.cpython-311.pyc b/wikideck/Mine/__pycache__/Block.cpython-311.pyc deleted file mode 100644 index ceec62d..0000000 Binary files a/wikideck/Mine/__pycache__/Block.cpython-311.pyc and /dev/null differ diff --git a/wikideck/Mine/__pycache__/Card.cpython-311.pyc b/wikideck/Mine/__pycache__/Card.cpython-311.pyc deleted file mode 100644 index d6efeee..0000000 Binary files a/wikideck/Mine/__pycache__/Card.cpython-311.pyc and /dev/null differ diff --git a/wikideck/Mine/__pycache__/Database.cpython-311.pyc b/wikideck/Mine/__pycache__/Database.cpython-311.pyc deleted file mode 100644 index 569047c..0000000 Binary files a/wikideck/Mine/__pycache__/Database.cpython-311.pyc and /dev/null differ diff --git a/wikideck/Mine/__pycache__/Mine.cpython-311.pyc b/wikideck/Mine/__pycache__/Mine.cpython-311.pyc deleted file mode 100644 index cb92f5f..0000000 Binary files a/wikideck/Mine/__pycache__/Mine.cpython-311.pyc and /dev/null differ diff --git a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc deleted file mode 100644 index 5ed8c6d..0000000 Binary files a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc and /dev/null differ diff --git a/wikideck/app.py b/wikideck/app.py index e0546be..612b87b 100644 --- a/wikideck/app.py +++ b/wikideck/app.py @@ -1,13 +1,17 @@ import dotenv import flask -import time +import os +ROLE = os.getenv('ROLE', 'mine') + +if ROLE == 'mine': + from Mine.Mine import mine + app = flask.Flask(__name__) + app.register_blueprint(mine) #from Market.Market import market -from Mine.Mine import mine - -app = flask.Flask(__name__) #app.register_blueprint(market, url_prefix="/market") -app.register_blueprint(mine, url_prefix="/mine") +else: + raise Exception("Environment variable ROLE must be either 'mine' or 'market'.") if __name__ == "__main__": dotenv.load_dotenv() diff --git a/wikideck/client.py b/wikideck/client.py deleted file mode 100644 index 2fb807d..0000000 --- a/wikideck/client.py +++ /dev/null @@ -1,52 +0,0 @@ -import json -import os -import requests - -from cryptography.hazmat.primitives.asymmetric import rsa - -from Mine.Block import Block -from Mine.Transaction import Transaction - -WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/') - -print("Generating RSA keys...") -privateKeyA = rsa.generate_private_key( - public_exponent=65537, - key_size=4096 - ) -privateKeyB = rsa.generate_private_key( - public_exponent=65537, - key_size=4096 - ) - -print("Getting block to mine from server...") -newBlockA = Block( - data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json() - ) -print("Received block for user A to mine.") -print("Mining block...") -newBlockA.mine(privateKeyA) -r = requests.post(f"{WIKIDECK_URL}/mine/blocks", json=newBlockA.as_dict()) -print(json.dumps(r.json(), indent=4)) -input("Press enter to continue...") - -print("Sending card A to user B...") -newTransaction = Transaction( - cardId = newBlockA.card.cardId, - sender = privateKeyA.public_key(), - receiver = privateKeyB.public_key(), - authorPrivateKey = privateKeyA - ) -r = requests.post(f"{WIKIDECK_URL}/mine/transactions", json=newTransaction.as_dict()) -print(json.dumps(r.json(), indent=4)) -input("Press enter to continue...") - -print("Getting block to mine from server...") -newBlockB = Block( - data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json() - ) -print("Received block for user B to mine.") -print("Mining block...") -newBlockB.mine(privateKeyB) -r = requests.post(f"{WIKIDECK_URL}/mine/blocks", json=newBlockB.as_dict()) -print(json.dumps(r.json(), indent=4))