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