diff --git a/docker-compose.yaml b/docker-compose.yaml index 1928219..9bced9f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: dockerfile: Dockerfile environment: ROLE: api - DATA_PATH: /data + DATA_PATH: /tmp DIFFICULTY_REQUIREMENT: 3 MINE_DB_HOST: mariadb-mine MINE_DB_USER: mine @@ -17,8 +17,6 @@ services: - mariadb-mine ports: - "8080:80" - volumes: - - ./mine_data:/data mariadb-mine: image: mariadb:latest @@ -29,6 +27,3 @@ services: MARIADB_PASSWORD: 123abc ports: - "3306:3306" - -volumes: - mine_data: diff --git a/wikideck/Mine/Block.py b/wikideck/Mine/Block.py index 744e51c..2e7536e 100644 --- a/wikideck/Mine/Block.py +++ b/wikideck/Mine/Block.py @@ -3,23 +3,26 @@ import hashlib import json import uuid +from cryptography.hazmat.primitives import serialization + from Mine.Card import Card from Mine.Transaction import Transaction class Block(): - def __init__(self, blockId=uuid.uuid4(), previousHash="0", - timestamp=datetime.datetime.now(datetime.timezone.utc), height=0, nonce=0, - difficulty=0, card=Card(), transactions=[], data=None): + def __init__(self, blockId=None, previousHash="0", timestamp=None, height=0, nonce=0, + difficulty=0, card=None, transactions=[], data=None): if data: self.load_from_data(data) else: - self.blockId = blockId + self.blockId = blockId if blockId else uuid.uuid4() self.previousHash = previousHash - self.timestamp = timestamp + self.timestamp = timestamp if timestamp else datetime.datetime.now( + datetime.timezone.utc + ) self.height = height self.difficulty = difficulty self.nonce = nonce - self.card = card + self.card = card if card else Card() self.transactions = transactions self.update() @@ -62,8 +65,14 @@ class Block(): if not self.nonce >= 0: raise self.Invalid("Nonce less than 0.") self.card.validate() + seenTransactions = [] for transaction in self.transactions: + if transaction in seenTransactions: + raise self.Invalid( + f"Contains duplicate transaction {transaction.transactionId}." + ) transaction.validate() + seenTransactions.append(transaction) # TODO validate that one transaction gives the card to the author def load_from_data(self, data): @@ -85,12 +94,12 @@ class Block(): transactionId = uuid.UUID(each['transactionId']), timestamp = datetime.datetime.strptime( each['timestamp'], - "%Y-%m-%d %H:%M:%S.%f" + "%Y-%m-%d %H:%M:%S.%f%z" ), cardId = uuid.UUID(each['cardId']), - sender = each['sender'], - receiver = each['receiver'], - signature = each['signature'] + sender = serialization.load_pem_public_key(each['sender'].encode('utf-8')), + receiver = serialization.load_pem_public_key(each['receiver'].encode('utf-8')), + signature = bytes.fromhex(each['signature']) ) for each in data['transactions'] ] self.update() @@ -110,9 +119,10 @@ class Block(): def __str__(self): # The hash of the block is the SHA256 hash of what this method returns. - return json.dumps(self.as_dict()) + return json.dumps(self.as_dict(), sort_keys=True) class Invalid(Exception): - def __init__(self, message, status_code=406): + def __init__(self, message, statusCode=406): super().__init__(message) - self.status_code = status_code + self.message = message + self.statusCode = statusCode diff --git a/wikideck/Mine/Card.py b/wikideck/Mine/Card.py index ffb2ac9..e0baee0 100644 --- a/wikideck/Mine/Card.py +++ b/wikideck/Mine/Card.py @@ -1,11 +1,13 @@ -import requests import json -import wikipedia +import requests +import time import uuid +import wikipedia class Card(): - def __init__(self, cardId=uuid.uuid4(), pageId=None, data=None): - self.cardId = cardId + def __init__(self, cardId=None, pageId=None, data=None): + self.cardId = cardId if cardId else uuid.uuid4() + delay = 2 while True: try: self.pageId = pageId if pageId else int( @@ -19,6 +21,10 @@ class Card(): except wikipedia.exceptions.DisambiguationError as e: # TODO pick random disambiuation option continue + except wikipedia.exceptions.WikipediaException as e: + time.sleep(delay) + delay = delay**2 + continue break if data: self.load_from_data(data) @@ -41,9 +47,10 @@ class Card(): } def __str__(self): - return json.dumps(self.as_dict()) + return json.dumps(self.as_dict(), sort_keys=True) class Invalid(Exception): - def __init__(self, message, status_code=406): + def __init__(self, message, statusCode=406): super().__init__(message) - self.status_code = status_code + self.message = message + self.statusCode = statusCode diff --git a/wikideck/Mine/Database.py b/wikideck/Mine/Database.py index 3013a5d..d6a41e9 100644 --- a/wikideck/Mine/Database.py +++ b/wikideck/Mine/Database.py @@ -1,44 +1,61 @@ +import base64 +import datetime import mariadb import os import time +import uuid + +from cryptography.hazmat.primitives import serialization from Mine.Block import Block +from Mine.Card import Card from Mine.Transaction import Transaction class Database(): - SQL_CREATE_DATABASE = [ + SQL_CREATE_TABLES = [ """ CREATE TABLE IF NOT EXISTS mine.blocks( sqlId INT PRIMARY KEY AUTO_INCREMENT, - blockId VARCHAR(37) UNIQUE NOT NULL, + blockId UUID UNIQUE NOT NULL, blockHash VARCHAR(64) UNIQUE NOT NULL, previousHash VARCHAR(64) UNIQUE NOT NULL, - timestamp DATETIME NOT NULL, + timestamp VARCHAR(32) NOT NULL, height INT UNIQUE NOT NULL, + difficulty INT 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, + blockId UUID UNIQUE NOT NULL, + cardId UUID UNIQUE NOT NULL, pageId INT NOT NULL, FOREIGN KEY (blockId) REFERENCES blocks(blockId) ); """, """ + CREATE TABLE IF NOT EXISTS mine.pending_transactions( + sqlId INT PRIMARY KEY AUTO_INCREMENT, + transactionId UUID UNIQUE NOT NULL, + cardId UUID NOT NULL, + timestamp VARCHAR(32) NOT NULL, + sender TEXT NOT NULL, + receiver TEXT NOT NULL, + signature TEXT NOT NULL, + FOREIGN KEY (cardId) REFERENCES cards(cardId) + ); + """, + """ 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, + blockId UUID NOT NULL, + transactionId UUID UNIQUE NOT NULL, + cardId UUID NOT NULL, + timestamp VARCHAR(32) NOT NULL, + sender TEXT NOT NULL, + receiver TEXT NOT NULL, + signature TEXT NOT NULL, FOREIGN KEY (blockId) REFERENCES blocks(blockId), FOREIGN KEY (cardId) REFERENCES cards(cardId) ); @@ -46,7 +63,7 @@ class Database(): """ CREATE TABLE IF NOT EXISTS mine.peers( sqlId INT PRIMARY KEY AUTO_INCREMENT, - peerId VARCHAR(37) UNIQUE NOT NULL, + peerId UUID UNIQUE NOT NULL, baseUrl VARCHAR(128) UNIQUE NOT NULL, isUp BOOLEAN NOT NULL, downCount INT DEFAULT 0, @@ -58,61 +75,89 @@ class Database(): SQL_GET_BLOCK_BY_ID = """ SELECT * FROM blocks - WHERE blockId = '{}'; + WHERE blockId = ?; """ - SQL_GET_LAST_BLOCK = """ - SELECT * - FROM blocks - ORDER BY timestamp DESC - LIMIT 1; - """ - - SQL_GET_CARD = """ - SELECT * + SQL_GET_CARD_BY_ID = """ + SELECT cardId, pageId FROM cards - WHERE cardId = '{}'; + WHERE cardId = ?; + """ + + SQL_GET_CARD_BY_BLOCK_ID = """ + SELECT cardId, pageId + FROM cards + WHERE blockId = ?; """ SQL_GET_CARD_OWNER = """ SELECT receiver FROM transactions - WHERE cardId = '{}' - AND isPending = False + WHERE cardId = ? ORDER BY timestamp DESC LIMIT 1; """ - SQL_GET_PENDING_TRANSACTIONS = """ - SELECT * - FROM transactions - WHERE isPending = True - ORDER BY timestamp ASC - LIMIT {}; - """ - - 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 mine.blocks ( + blockId, blockHash, previousHash, timestamp, height, difficulty, nonce + ) + VALUES (?, ?, ?, ?, ?, ?, ?); + """ + + SQL_GET_LAST_BLOCK = """ + SELECT blockId, previousHash, timestamp, height, difficulty, nonce + FROM mine.blocks + ORDER BY timestamp DESC + LIMIT 1; + """ + + SQL_INSERT_CARD = """ + INSERT INTO mine.cards (blockId, cardId, pageId) VALUES (?, ?, ?); + """ + + SQL_INSERT_PENDING_TRANSACTION = """ + INSERT INTO mine.pending_transactions ( + transactionId, timestamp, cardId, sender, receiver, signature + ) + VALUES (?, ?, ?, ?, ?, ?); + """ + + SQL_GET_PENDING_TRANSACTIONS = """ + SELECT transactionId, timestamp, cardId, sender, receiver, signature + FROM mine.pending_transactions + ORDER BY timestamp ASC + LIMIT ?; + """ + + SQL_DELETE_PENDING_TRANSACTION = """ + DELETE FROM mine.pending_transactions WHERE transactionId = ?; """ SQL_INSERT_TRANSACTION = """ - INSERT INTO transactions ( - transactionId, - timestamp, - sender, - receiver, - signature, - blockId, - cardId + INSERT INTO mine.transactions ( + blockId, transactionId, timestamp, cardId, sender, receiver, signature ) - VALUES ({}, {}, {}, {}, {}); + VALUES (?, ?, ?, ?, ?, ?, ?); + """ + + SQL_GET_PENDING_TRANSACTION_BY_ID = """ + SELECT transactionId, timestamp, cardId, sender, receiver, signature + FROM pending_transactions + WHERE transactionId = ?; + """ + + SQL_GET_TRANSACTION_BY_ID = """ + SELECT transactionId, timestamp, cardId, sender, receiver, signature + FROM transactions + WHERE transactionId = ?; + """ + + SQL_GET_TRANSACTIONS_BY_BLOCK_ID = """ + SELECT transactionId, timestamp, cardId, sender, receiver, signature + FROM mine.transactions + WHERE blockId = ? + ORDER BY timestamp DESC; """ def __init__(self): @@ -124,67 +169,229 @@ class Database(): port = os.getenv('MINE_DB_PORT', 3306), user = os.getenv('MINE_DB_USER', None), password = os.getenv('MINE_DB_PASSWORD', None), - database = 'mine' + database = 'mine', + autocommit = True ) - for each in self.SQL_CREATE_DATABASE: - cursor = self.conn.cursor().execute(each) + for each in self.SQL_CREATE_TABLES: + 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) + cur = self.conn.cursor() + cur.execute(self.SQL_GET_BLOCK_BY_ID, [blockId]) + block = cur.fetchone() + if block: + blockCard = self.get_card_by_block_id(lastBlock[1]) + blockTransactions = self.get_transactions_by_block_id(lastBlock[1]) + return Block( + blockId = uuid.UUID(block[1]), + previousHash = block[3], + timestamp = block[4], + height = block[5], + nonce = block[6], + difficulty = block[7], + card = blockCard, + transactions = blockTransactions ) + else: + return False + + def get_card_by_id(self, cardId): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_CARD_BY_ID, [str(cardId)]) + card = cur.fetchone() + return Card( + cardId = uuid.UUID(card[0]), + pageId = card[1] + ) + + def get_card_by_block_id(self, blockId): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_CARD_BY_BLOCK_ID, [str(blockId)]) + card = cur.fetchone() + return Card( + cardId = uuid.UUID(card[0]), + pageId = card[1] ) def get_last_block(self): - 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(self.SQL_GET_CARD.format(cardId)) - ) + cur = self.conn.cursor() + cur.execute(self.SQL_GET_LAST_BLOCK) + lastBlock = cur.fetchone() + if lastBlock: + lastBlockCard = self.get_card_by_block_id(lastBlock[0]) + lastBlockTransactions = self.get_transactions_by_block_id(lastBlock[0]) + return Block( + blockId = uuid.UUID(lastBlock[0]), + previousHash = lastBlock[1], + timestamp = datetime.datetime.strptime( + lastBlock[2], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + height = lastBlock[3], + difficulty = lastBlock[4], + nonce = lastBlock[5], + card = lastBlockCard, + transactions = lastBlockTransactions + ) def get_card_owner(self, cardId): - return self.conn.cursor().execute( - self.SQL_GET_CARD_OWNER.format(cardId) - ) + cur = self.conn.cursor() + cur.execute(self.SQL_GET_CARD_OWNER, [str(cardId)]) + owner = cur.fetchone() + return serialization.load_pem_public_key( + owner[0].encode('utf-8') + ) if owner else None def get_pending_transactions(self, limit=32768): - pendingTransactions = self.conn.cursor().execute( - self.SQL_GET_PENDING_TRANSACTIONS.format(limit) + cur = self.conn.cursor() + cur.execute( + self.SQL_GET_PENDING_TRANSACTIONS, + [limit] ) + pendingTransactions = cur.fetchall() return [ Transaction( - data=each - ) for each in pendingTransactions + transactionId = uuid.UUID(pendingTransaction[0]), + timestamp = datetime.datetime.strptime( + pendingTransaction[1], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + cardId = uuid.UUID(pendingTransaction[2]), + # TODO: load rsa keys + sender = serialization.load_pem_public_key( + pendingTransaction[3].encode('utf-8') + ), + receiver = serialization.load_pem_public_key( + pendingTransaction[4].encode('utf-8') + ), + signature = bytes.fromhex(pendingTransaction[5]) + ) for pendingTransaction in pendingTransactions ] if pendingTransactions else [] - def get_transaction(self, transactionId): - return Transaction( - data=self.conn.cursor().execute(self.SQL_GET_TRANSACTION.format(transactionId)) - ) + def get_pending_transaction_by_id(self, transactionId): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_PENDING_TRANSACTION_BY_ID, [str(transactionId)]) + transaction = cur.fetchone() + if transaction: + return Transaction( + transactionId = transaction[0], + timestamp = datetime.datetime.strptime( + transaction[1], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + cardId = uuid.UUID(transaction[2]), + sender = serialization.load_pem_public_key( + transaction[3].encode('utf-8') + ), + receiver = serialization.load_pem_public_key( + transaction[4].encode('utf-8') + ), + signature = bytes.fromhex(transaction[5]) + ) + + def get_transaction_by_id(self, transactionId): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_TRANSACTION_BY_ID, [str(transactionId)]) + transaction = cur.fetchone() + if transaction: + return Transaction( + transactionId = transaction[0], + timestamp = datetime.datetime.strptime( + transaction[1], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + cardId = uuid.UUID(transaction[2]), + sender = serialization.load_pem_public_key( + transaction[3].encode('utf-8') + ), + receiver = serialization.load_pem_public_key( + transaction[4].encode('utf-8') + ), + signature = bytes.fromhex(transaction[5]) + ) + + def get_transactions_by_block_id(self, blockId): + cur = self.conn.cursor() + cur.execute(self.SQL_GET_TRANSACTIONS_BY_BLOCK_ID, [blockId]) + transactions = cur.fetchall() + return [ + Transaction( + transactionId = transaction[0], + timestamp = datetime.datetime.strptime( + transaction[1], + "%Y-%m-%d %H:%M:%S.%f%z" + ), + cardId = transaction[2], + sender = serialization.load_pem_public_key( + transaction[3].encode('utf-8') + ), + receiver = serialization.load_pem_public_key( + transaction[4].encode('utf-8') + ), + signature = bytes.fromhex(transaction[5]) + ) for transaction in transactions + ] def insert_block(self, block): - self.conn.cursor().execute(self.SQL_INSERT_BLOCK.format( + cur = self.conn.cursor() + cur.execute(self.SQL_INSERT_BLOCK, ( str(block.blockId), block.blockHash.hexdigest(), block.previousHash, str(block.timestamp), block.height, + block.difficulty, block.nonce )) - def insert_transaction(self, transaction): - return self.conn.cursor().execute(self.SQL_INSERT_TRANSACTION.format( + def insert_card(self, blockId, card): + cur = self.conn.cursor() + cur.execute(self.SQL_INSERT_CARD, ( + str(blockId), + str(card.cardId), + card.pageId + )) + + def insert_pending_transaction(self, transaction): + cur = self.conn.cursor() + cur.execute(self.SQL_INSERT_PENDING_TRANSACTION, ( str(transaction.transactionId), str(transaction.timestamp), - transaction.sender, - transaction.receiver, - transaction.signature, - str(transaction.cardId) + str(transaction.cardId), + transaction.sender.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + transaction.receiver.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + transaction.signature.hex() )) + + def delete_pending_transaction(self, transactionId): + cur = self.conn.cursor() + cur.execute(self.SQL_DELETE_PENDING_TRANSACTION, [str(transactionId)]) + + def insert_transaction(self, blockId, transaction): + cur = self.conn.cursor() + cur.execute(self.SQL_INSERT_TRANSACTION, ( + str(blockId), + str(transaction.transactionId), + str(transaction.timestamp), + str(transaction.cardId), + transaction.sender.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + transaction.receiver.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8'), + transaction.signature.hex() + )) + diff --git a/wikideck/Mine/Mine.py b/wikideck/Mine/Mine.py index d56a4d2..3afe1a7 100644 --- a/wikideck/Mine/Mine.py +++ b/wikideck/Mine/Mine.py @@ -1,6 +1,8 @@ +import datetime import flask import json import os +import uuid from cryptography.hazmat.primitives.asymmetric import rsa @@ -21,19 +23,27 @@ 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)) # Blocks are json strings. This is the true block. + f.write(str(block)) + # TODO: error handling (don't reveal mariadb errors) 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 + # TODO update peers -if not db.get_last_block(): - # TODO: load blocks from files - # TODO: try to get blocks from peers +def generate_origin_block(): originBlock = Block( difficulty = DIFFICULTY_REQUIREMENT, + card = Card(pageId=18618509) # You can target specific cards ) originBlock.mine(privateKey) originBlock.validate() save_block(originBlock) + return originBlock @mine.get('/') def index_get(): @@ -42,6 +52,7 @@ def index_get(): ### # Retrieve blocks and block data. +# Creates an origin block if none exists # Returns a skeleton block to be mined by default. # Queries for a specific block when given parameters. ### @@ -49,17 +60,16 @@ def index_get(): def blocks_get(): # TODO: block queries blockId = flask.request.args.get('blockId', None) - lastBlock = db.get_last_block() + if not (lastBlock := db.get_last_block()): + # TODO: load blocks from files + # TODO: try to get blocks from peers + lastBlock = generate_origin_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 - ) - ] + transactions = db.get_pending_transactions() ).as_dict() ) @@ -71,16 +81,16 @@ def blocks_get(): @mine.post('/blocks') def blocks_post(): try: - newBlock = Block(data=request.get_json()) + newBlock = Block(data=flask.request.get_json()) newBlock.validate() previousBlock = db.get_last_block() - if newBlock.previousHash != previousBlock.blockHash: + if newBlock.previousHash != previousBlock.blockHash.hexdigest(): raise Block.Invalid( - f"Incorrect previous hash - should be {previousBlock.blockHash}." + f"Incorrect previous hash - should be {previousBlock.blockHash.hexdigest()}." ) if newBlock.timestamp <= previousBlock.timestamp: raise Block.Invalid( - "Timestamp is later than previous block." + f"Timestamp {newBlock.timestamp} is before {previousBlock.timestamp}." ) if newBlock.height != previousBlock.height + 1: raise Block.Invalid( @@ -95,48 +105,46 @@ def blocks_post(): "Block contains no transactions." ) for transaction in newBlock.transactions: - pendingTransaction = db.get_transaction(transaction.transactionId) + if (transaction.sender == transaction.receiver + and transaction.cardId != newBlock.card.cardId): + raise Transaction.Invalid( + "Recursive transactions are only allowed to collect mining reward." + ) + pendingTransaction = db.get_pending_transaction_by_id(transaction.transactionId) if not pendingTransaction: - raise Transaction.Invalid( - f"No matching pending transaction for {transaction.transactionId}." - ) - if not pendingTransaction.pending: - raise Transaction.AlreadyFulfilled( - f"Transaction {transaction.transactionId} has already been fulfilled." - ) - if transaction.timestamp != pendingTransaction.timestamp: - raise Transaction.Invalid( - f"Incorrect timestamp on {transaction.transactionId}." - ) - if transaction.cardId != pendingTransaction.cardId: - raise Transaction.Invalid( - f"Incorrect cardId on {transaction.transactionId}." - ) - if transaction.sender != pendingTransaction.sender: - raise Transaction.Invalid( - f"Incorrect sender on {transaction.transactionId}." - ) - if transaction.recipient != pendingTransaction.recipient: - raise Transaction.Invalid( - f"Incorrect recipient on {transaction.transactionId}." - ) - if transaction.signature != pendingTransaction.signature: - raise Transaction.Invalid( - f"Incorrect signature on {transaction.transactionId}." - ) - save_block(block) - for transaction in newBlock.transactions: - db.update_transactions_is_pending_false(transaction.transactionId) - # TODO: update peers - return flask.jsonify(block.asDict()) - except Transaction.Invalid as e: - return e, e.statusCode - except Transaction.AlreadyFulfilled as e: - return e, e.statusCode - except Card.Invalid as e: - return e, e.statusCode - except Block.Invalid as e: - return e, e.statusCode + if transaction.cardId != newBlock.card.cardId: + raise Transaction.Invalid( + f"No matching pending transaction for {transaction.transactionId}." + ) + else: + if transaction.timestamp != pendingTransaction.timestamp: + raise Transaction.Invalid( + f"Incorrect timestamp on {transaction.transactionId}." + ) + if transaction.cardId != pendingTransaction.cardId: + raise Transaction.Invalid( + f"Incorrect cardId on {transaction.transactionId}." + ) + if transaction.sender != pendingTransaction.sender: + raise Transaction.Invalid( + f"Incorrect sender on {transaction.transactionId}." + ) + if transaction.receiver != pendingTransaction.receiver: + raise Transaction.Invalid( + f"Incorrect receiver on {transaction.transactionId}." + ) + if transaction.signature != pendingTransaction.signature: + raise Transaction.Invalid( + f"Incorrect signature on {transaction.transactionId}." + ) + save_block(newBlock) + return flask.jsonify(newBlock.as_dict()) + except Exception as e: + return flask.jsonify( + {'Error': str(e)} + ), e.statusCode if hasattr( + e, 'statusCode' + ) else 500 ### # Retrieve card data @@ -153,29 +161,33 @@ def cards_get(): # This method performs a number of validations on the submitted transaction and returns # a status code result. ### -@mine.put('/transactions') -def transactions_put(): +@mine.post('/transactions') +def transactions_post(): try: - newTransaction = Transaction(data=request.get_json()) + newTransaction = Transaction(data=flask.request.get_json()) newTransaction.validate() - if not get_card(newTransaction.cardId): + if not db.get_card_by_id(newTransaction.cardId): raise Transaction.Invalid( f"Card {newTransaction.cardId} does not exist.", 404 ) - if newTransaction.sender != get_card_owner(newTransaction.cardId): + if newTransaction.sender != db.get_card_owner(newTransaction.cardId): raise Transaction.Unauthorized( f"{newTransaction.sender} does not own {newTransaction.cardId}." ) - insert_transaction(newTransaction) + if newTransaction.sender == newTransaction.receiver: + raise Transaction.Invalid( + "Recursive transaction are not accepted at this endpoint." + ) + db.insert_pending_transaction(newTransaction) # TODO: update peers? - return 200 - except mariadb.Error as e: - return e, 500 - except Transaction.Unauthorized as e: - return e, e.statusCode - except Transaction.Invalid as e: - return e, e.statusCode + return flask.jsonify(newTransaction.as_dict()) + except Exception as e: + return flask.jsonify( + {'Error': str(e)} + ), e.statusCode if hasattr( + e, 'statusCode' + ) else 500 ### # Retrieve a transaction. diff --git a/wikideck/Mine/Transaction.py b/wikideck/Mine/Transaction.py index cab0511..c5fbc12 100644 --- a/wikideck/Mine/Transaction.py +++ b/wikideck/Mine/Transaction.py @@ -3,17 +3,18 @@ import io import json 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 class Transaction(): - def __init__(self, transactionId=uuid.uuid4(), - timestamp=datetime.datetime.now(datetime.timezone.utc), - cardId=None, sender=None, receiver=None, authorPrivateKey=None, signature=None, - data=None): - self.transactionId = transactionId - self.timestamp = timestamp + def __init__(self, transactionId=None, timestamp=None, cardId=None, sender=None, + receiver=None, authorPrivateKey=None, signature=None, data=None): + self.transactionId = transactionId if transactionId else uuid.uuid4() + self.timestamp = timestamp if timestamp else datetime.datetime.now( + datetime.timezone.utc + ) self.cardId = cardId self.sender = sender self.receiver = receiver @@ -29,7 +30,6 @@ class Transaction(): # TODO: validate cardId # TODO: validate sender # TODO: validate receiver - # TODO: validate signature try: self.sender.verify( self.signature, @@ -42,7 +42,7 @@ class Transaction(): encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode('utf-8') - }).encode('utf-8') + }, sort_keys=True).encode('utf-8') ), padding.PSS( mgf=padding.MGF1(hashes.SHA256()), @@ -51,7 +51,7 @@ class Transaction(): hashes.SHA256() ) except Exception as e: - raise self.InvalidSignature("Invalid signature.") + raise self.InvalidSignature(str(e)) def sign(self, authorPrivateKey): self.signature = authorPrivateKey.sign( @@ -64,7 +64,7 @@ class Transaction(): encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode('utf-8') - }).encode('utf-8') + }, sort_keys=True).encode('utf-8') ), padding.PSS( mgf=padding.MGF1(hashes.SHA256()), @@ -80,21 +80,31 @@ 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'] + # TODO: why is this sometimes a tuple? + self.sender = serialization.load_pem_public_key( + data['sender'].encode('utf-8'), + backend=default_backend() + ), + if isinstance(self.sender, tuple): + self.sender = self.sender[0] + self.receiver = serialization.load_pem_public_key( + data['receiver'].encode('utf-8'), + backend=default_backend() + ), + if isinstance(self.receiver, tuple): + self.receiver = self.receiver[0] + self.signature = bytes.fromhex(data['signature']) def as_dict(self): return { "transactionId": str(self.transactionId), "timestamp": str(self.timestamp), "cardId": str(self.cardId), - "receiver": self.receiver.public_bytes( + "sender": self.sender.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode('utf-8'), - "sender": self.sender.public_bytes( + "receiver": self.receiver.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ).decode('utf-8'), @@ -102,29 +112,34 @@ class Transaction(): } def __str__(self): - return json.dumps(self.as_dict()) + return json.dumps(self.as_dict(), sort_keys=True) class Unauthorized(Exception): def __init__(self, message, statusCode=403): super().__init__(message) + self.message = message self.statusCode = statusCode class Invalid(Exception): def __init__(self, message, statusCode=400): super().__init__(message) + self.message = message self.statusCode = statusCode class AlreadyFulfilled(Exception): def __init__(self, message, statusCode=400): super().__init__(message) + 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) + self.message = message self.statusCode = statusCode diff --git a/wikideck/Mine/__pycache__/Block.cpython-311.pyc b/wikideck/Mine/__pycache__/Block.cpython-311.pyc index f39ee83..ceec62d 100644 Binary files a/wikideck/Mine/__pycache__/Block.cpython-311.pyc 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 index 17865fb..d6efeee 100644 Binary files a/wikideck/Mine/__pycache__/Card.cpython-311.pyc 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 index 28064d5..569047c 100644 Binary files a/wikideck/Mine/__pycache__/Database.cpython-311.pyc and b/wikideck/Mine/__pycache__/Database.cpython-311.pyc differ diff --git a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc index 073a521..5ed8c6d 100644 Binary files a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc and b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc differ diff --git a/wikideck/client.py b/wikideck/client.py index 1479dbb..2fb807d 100644 --- a/wikideck/client.py +++ b/wikideck/client.py @@ -5,19 +5,48 @@ 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/') -privateKey = rsa.generate_private_key( +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 ) -newBlock = Block( +print("Getting block to mine from server...") +newBlockA = Block( data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json() ) -newBlock.mine(privateKey) -print(newBlock) +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...") -r = requests.post(f"{WIKIDECK_URL}/mine/blocks", data=newBlock.as_dict()) -print(r) +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))