From 05fc8f9a46c7b0819537f40092b8309a34ceb73e Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Wed, 4 Jun 2025 11:47:49 -0400 Subject: [PATCH] Unit testing basic workflows --- db-old/createDatabase.sql | 51 ---- db-old/getBuyOrders.sql | 12 - db-old/getDeck.sql | 10 - db-old/getSellOrders.sql | 11 - db/createMineDatabase.sql | 38 --- docker-compose.yaml | 6 +- static/client.js | 0 test/Mine/TestMine.py | 261 ++++++++++++++++++ .../__init__.py | 0 test/test.py | 10 + wikideck/Mine/Block.py | 44 ++- wikideck/Mine/Card.py | 10 +- wikideck/Mine/Database.py | 67 ++++- wikideck/Mine/Mine.py | 51 +++- wikideck/Mine/Transaction.py | 63 ++++- .../Mine/__pycache__/Block.cpython-311.pyc | Bin 7936 -> 0 bytes .../Mine/__pycache__/Card.cpython-311.pyc | Bin 3469 -> 0 bytes .../Mine/__pycache__/Database.cpython-311.pyc | Bin 11989 -> 0 bytes .../Mine/__pycache__/Mine.cpython-311.pyc | Bin 8948 -> 0 bytes .../__pycache__/Transaction.cpython-311.pyc | Bin 9137 -> 0 bytes wikideck/app.py | 14 +- wikideck/client.py | 52 ---- 22 files changed, 477 insertions(+), 223 deletions(-) delete mode 100644 db-old/createDatabase.sql delete mode 100644 db-old/getBuyOrders.sql delete mode 100644 db-old/getDeck.sql delete mode 100644 db-old/getSellOrders.sql delete mode 100644 db/createMineDatabase.sql delete mode 100644 static/client.js create mode 100644 test/Mine/TestMine.py rename db-old/getLastTransactionForCard.sql => test/__init__.py (100%) create mode 100644 test/test.py delete mode 100644 wikideck/Mine/__pycache__/Block.cpython-311.pyc delete mode 100644 wikideck/Mine/__pycache__/Card.cpython-311.pyc delete mode 100644 wikideck/Mine/__pycache__/Database.cpython-311.pyc delete mode 100644 wikideck/Mine/__pycache__/Mine.cpython-311.pyc delete mode 100644 wikideck/Mine/__pycache__/Transaction.cpython-311.pyc delete mode 100644 wikideck/client.py 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 ceec62ddb61c0b8cd0c8bb700152df31a087809a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7936 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 H)kXF{CBGU~ diff --git a/wikideck/Mine/__pycache__/Card.cpython-311.pyc b/wikideck/Mine/__pycache__/Card.cpython-311.pyc deleted file mode 100644 index d6efeee1ec728b36417b5667f00706cc90cab32a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3469 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 diff --git a/wikideck/Mine/__pycache__/Mine.cpython-311.pyc b/wikideck/Mine/__pycache__/Mine.cpython-311.pyc deleted file mode 100644 index cb92f5f1c506e9bbb509aa0ee21a2f0454215128..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8948 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 diff --git a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc deleted file mode 100644 index 5ed8c6dd7539e5634d00198f7698ac014e53d53e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9137 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 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))