From 1c31de2bb4b4c65be84c0780b4e221d9f6b1c054 Mon Sep 17 00:00:00 2001 From: Eric Meehan Date: Tue, 3 Jun 2025 11:20:07 -0400 Subject: [PATCH] Functional prototype --- docker-compose.yaml | 7 +- wikideck/Mine/Block.py | 36 +- wikideck/Mine/Card.py | 21 +- wikideck/Mine/Database.py | 375 ++++++++++++++---- wikideck/Mine/Mine.py | 148 +++---- wikideck/Mine/Transaction.py | 49 ++- .../Mine/__pycache__/Block.cpython-311.pyc | Bin 7168 -> 7936 bytes .../Mine/__pycache__/Card.cpython-311.pyc | Bin 3154 -> 3469 bytes .../Mine/__pycache__/Database.cpython-311.pyc | Bin 5542 -> 11989 bytes .../__pycache__/Transaction.cpython-311.pyc | Bin 8168 -> 9137 bytes wikideck/client.py | 41 +- 11 files changed, 476 insertions(+), 201 deletions(-) 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 f39ee832e9d9b11b08c732927e2bd7f056892fbf..ceec62ddb61c0b8cd0c8bb700152df31a087809a 100644 GIT binary patch delta 3141 zcmb7GTWlN06`fr!U*b}v9+oIk59&e2t4gd>D~%;favZBk?L>@YTTP@yW*P2UqRm%l zmxirUpircR5x9WW`LGhFwZGgLb^+&6BtRc#ZSB{%DNNEi^3E1_X8KiP1wUQKhsi)&3??9oI!dZo+Go=^9lyP88>tD8>}Q^y(T-CDN(+gptZ+m);sE%|Xj$qLFDq zCDWoRsQB9WPnadDaLTPpZwk{k9DZ6-9e{S#3FuH=fKJs7=u&F{-P{Zhm?3L5*KJ@3 zyjGd^a6`PN{n>Obc_wBGdnj6&Z01Yb&_Z>5zd9$d-wAu0e_j-*SQN7LMs*-UPK&I| z(G)F`c(t-9f+TGWLW(x%WXld1+lF5HA|!8@nN#R~20FQ`og|Q|9f}T+Jk>TkNODzj z!tl$E$= zp^#EdJK`X_CI*;aZf75h5j(}@u=`@T7l)@fDs4pAh0u)9gb+YzVG-M@I9B6id-Pc2XV zAy64Qa&IVhcPMt>AE@}dD*o_8n>bwmhyXyZ*qVa`yV#1Y!58W+2lgyW%hGRpZ$xfI z*xzj*3+wDx(qK;zjSv7rv>lMP0?f;N)1!qvtXXSk|B?oU&JX+S2gOngG@Fh@KCfj} zYR3+IK?+varXM2eHsegGZ06|zh) zrA#WT&DU5q;CBc$J_lH2|8#!t=!CDNz;1gRh02HjaD~N=S+XD$$?59Gj+2>*1rb;# zoF%k_t+`vmHAV4sb*rxs)$>c+by)UITskj55sD%Uc_LC#03Q6vzbhi{R1nC;7!pH~ zV{`zz&?v&U*=wFlrTNZ7xvY^$W%ZC+$b%EWZy|#wvU(zkfg4&EqDmdzi#^aQbPxdv zVLA_I&q9E!7F+ySUx!eS(15VTAoR8A;_5H~<(uJ!X=XO(IjqtrK}WH1*P4D^)3PVF z%+O8GHluWm$Iyn?=@1C>wU*O2XLt(g^c?${{HWAkb_G_0ot0q!8gaOv;d#qtJl;6nwm9m@CuX(7DsT@eOAi1fs}umi3oknTt52c!GMObsGV|GjU~K5H z9{&fT;bE8DjgF$AN-c!F>+WE#8b2#awcA=8J6k*kZmd<<55;C6nd)Q*{ejXg*L4Tb z5MHY+i5DLL$J}%nWQ!AH_{Gib!>=xcZvmKYJvEz67zL^+j&1r|s;ET|0w^_Buwu$F zrOC;|;}i`tT`xjd=8aTFqeup`fv3B?mWgl0G7V#k{f+a6rkhSog%Hs9;OyMiT8Tn z`*Edj|4Q3PrEP?z>pM%B3V0YOwG`JzpGDXr2@j`3Shtmnw`hSWdm|y^*Y>2;1{KwH zVIv+c^Nm(`RL=wlKNMoj-|!_6biLu2Q2+ebJ04*tcSQt){dAYY%~f36?0cM5)(HW_ zz^3)$)LYB>M)er#VLG5s0Bq0a*fQ_6>~k#K=jc!1A=!bHurbyOlGX)BCnxz8)2S4vR z*js_1V*zUQ0d_Oc*&6KT2X4o1_3lUh0*k3-vQ z*dKGmGuA1(4?gK6KoJ{80sg%R%eH=NqRp+Wk^cj@2^0q{DKfxXYnQLd4u?Y(zQeAyhDyVb`RyCvlI*bf|AkA+L#%!D zm?q0qG*I#IyhXd=o4$;|Gg?QIp3fV(S(?btosZ5X<}(Q+ny0Bu%1E8n^hqnZxhLQ* z5j~WkD!;6melBl29<3b7y|L3n>#mVZPA#OhF{;9(xY7C*mJ3Gx`^sd;!>$A)2MU4! QOME;&@~ delta 2515 zcmb7FUrbYH6#s79TW-sx1%%SSv;svh3WBoC{~L-R{s9$Tw3#85dx0Xf?7PJ{QU^t<1rlfd)qd)o$DN+w-i;rQg59kmwHPE^}@qKY#XLTpvpGAhaYbe!w+7KEk#?aY|!LC{mA}EfZr#}-mC~O+voDnjlM_6QMEI{Q$&sc%V z8_3vzvgIZ1K#6%u1j?SL3V_OIAR_@K<#}YNB-cID=dE#``=j7u1a`>Jcp0>@&(X1Su>d|XyAK4+=gWc_7>!%` z@tCw+`~Y$+8AZ!-&|zk9tSow>J~~_0}SfeZn60qWlpI+V7qtLg~)`%CxY(-df=_OL>)X^h}YKx$-Ip%uRxlP8vij zOP;@0d=JWDv(qrZ^1XnZu_Cj%rMTXsCVJDSSlE{|=Tz(j{$=)Av6ubii!xuSYi&27 zxGxo}VH?y_NkddeheveEO)S^WHuwYf6q@t1L;kYDIk%E^E3=Y9y)5ld2*=q2|GwQT z);mu6qH<9m9nmPQ00l;&o`&AidwNr}j;A zu)Be*d-Gsm!3&Z6-MeE@-&UX*RKMXU!9dI%n zd(QT1Fes@cw?tc!&6}Dh0jHWGV3qX802BFFF05QhPmNv8Ol9s==K|HRy6y)W?*$sa z@n)NLWt-aO0`1vA`|P2^vnOJ?V0CD^b-ML>+l{t6r9afni!RYKPgX*NZ3(S*7mKj{ z@=(hXp~!e0WKLWF$tpr3UxrJ{6TF2feu-~JgmEYiGhco<({==&O2$Lt@f4DTJbmDB;p5GKEq>Erexd)~!<~PG{F7 z4G#809`cX~n`zY)AptKzQBuJlKoEFGDT;)(QYBt_b47$A!~g$f@tgUc=M;hnPDo)XKhccke5~X0IN01nItpWg z!U9o=u!O@R5tmR{A`+lXWIzXT06GQq1|rTSL=;_F7ZKWof2oC4g0CRWmES+n_B-Y= z)0hfrJX3FiRBlt4Vbwd4MiHT`B6v|!k&4m+mhO}zS^R%=MWNlUm!ze8gyZ}d7lqxAcD5p4w=EZAh4g+^0^`+5Q zZiLv7(C5>H&>x&%&0|TaRGe5c$3YeoCf~1rFMcyVtCM*2wfRYX){G}-EXj;d>y~UJ zbbZ#UO(dfv5~IoK2#K0e&0$G~o`_M-9fci`C2_qa6Fm{VZDHMTfHJ`Lg|~y8Yoj01 z&!R%0d&m1yE|!Zu7~2}(nAnuzGik4`Y^f2NEwj&z6Fd!E|%NaEyBtB^_RNL_mq~}@A*@rvX_s(Yg4*ShH@;bp= z!a5vcv-05Jm5QNvHEgLylA4j5`fWoKEosVtN{~rvde)#lJlk%7@{TN|wDg|gicqZlaIRPIBOm8>ke7m%%y8PizCPL?aK7t@NVcmAi}RI1I4@BE0}gPOottpeSOD3#ck<_X2Wf?S4d4S@P_lgI`fPgCYMX;W5|kJ!OG* H^1AyMS_X)C`@j-Ae~ znMMVKuEo*!-nR|y4N@i7bBt@O7SD+Z98e*L4hz5-zCS>S1hBn;f9TkJ) zw;SA~ywhJYn6YZrmSW$g=X?c?@;iM+rs@dCf)$ZrW=T2?Q@ z62W{_cCZ1rll+n1&3{$$`X)JNnY^Wxj%F$~huX8OwqR4ocPu^7Jhw8-1VNTY2sEJ& zi0g?0@O$b9d13ROAT(hs*MC)8Geaev*cU1$Kc-zzhPsmOZM9lqjGOvoUk@()at3Bh zcwly)($xn;2N8b({glo>)$fuN&%`D_6zMS0B<)QTZ!eeJMSs3l4dQ32pF5R#YAI}= zuy-SPqE`w9b^w2YAv_(c%f`iugl(59j^`l@kyfElXv))IIsP=6j~zvwXWcLdbAzK- zPd`reKa?*MHL(b@`0M8As@y8Q8>r+~Dz=Tb3E?WrHr)=`iy}LMAPpx`Xf-j}_6QXv zF&4uA07-#~psQAj37%|Hz-t2f5dvLw$&@Sv6NP160_2g52uU=w46q69V4@3|sDcD> zp5ASQh$G&Tfg?BRM)_7{qw?d_eHucSJ`zG!Vh2WeF?DQpGz6mWLQK^$w`buiYg=e{ zo?~hg5?t^m8_fNz?s~q*hET!e6%!e@xlRd5oQEv)U&b2?HCi96xNoxKs1yg_eS#3G igpe)BHs#yYSU^e9lwK0MWpM`Cq+j)*bZuk%1c}4*M diff --git a/wikideck/Mine/__pycache__/Database.cpython-311.pyc b/wikideck/Mine/__pycache__/Database.cpython-311.pyc index 28064d524b5c46e1cccd0bcebd86898010ebb7b6..569047cde0a3ecf4f21d32d630aca887f62b900a 100644 GIT binary patch literal 11989 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 literal 5542 zcmd5<&2JmW6`v)SU(!<4Qj%p`DZPqnTa+2eO`6(P9I5)CB|$Xhh_u}hNES4AEzy!l zb$8_tMJ|NGhoI1*1&q3f9CT7@$OVf21qOQQsR)TRC@f&0Ku_(7g58TxeQ%b#TvCyv z9#Uio{&wcg+nG1-{pRh=*TG;5fgh0SEHCZ!Li)_9eH$`c}!3?mWL&$uO5hLzkIkHluY zk|*Pnycxgb%LF9Z}=y(TJ}~} zmDgum1SqL^B0iB82595T{M=RH)^ahoq|)}>{A7Gy82?nz@+-2cWmi^($#`ml_9bSn z&ZLE(d_)`Tm+0f*V7aU!nPc6Yj?c#hY|lu-#~%w{JgmL?8|SNvoRjl+WrfP9Ihv9! zsM(yBFBT3Ph$@dICxyIvO)g0J!U9fkJG-pPwCm{0h&yd@_~Z~H8s^iKHMzE=EC1Ut zj?pdO&;izeMISTCRD3=y%p}uuwcrT-G)Oa2%oI##)y0^ws>pZq#WmGz*&&Mwi*kNp zQHu$MVj(9->6kah64&D?q5q4AF^EL-KU!PI-w(|1FjW=$X*aUSAqJ!BVv9C)kqQBl zm3rF>cwM6A7YbQzO_A#&Yqj>kCXMXocdeZ0pFIiuZr@7h=@brW`x!LH;Ba@1q6i$zs4 z*wvx}xTeaA;a|b;^O8GfmjVWJ+z2mUgAxniLp=QP7rR1NQftjUGD28+U{jKQk% z@@*9zF1-10XtB5=56MbCH#8}$OImSt=w5y)FUh&3dgUsZFl1%1!PN(bUlj9&ye5k4 zye+mwmtbJ^bAV0qva{#$+{=!x$Cv)_`5!+mbzIOpE?CWUspEaU$cUdCdN`}aXc|MF{^ZerWrG(8((UJpo+^`RTJq>UGKKz z(AY7K?~MRJG(w$weAnX>6~4F3_m=oRo$ssg=gR!K7qO8NKdSSi&l#N`uex3B;BbJ~ z2vy<=v?;j0oYQm2GwSWXK;`@P_O`v*U^2UGFpZvWY`r}uN)HSZ^&0F1d>&~Wlv5y4 z1OS7x0x&TKJJ5T(sH|kQr!M6fc6!ZBsG>6j!Hk9Yl z=(Wey&Li`Alle{;q9Z8t??VO7qRgZ3!QG8qo@@#ib>@NO*8%pt8l*-ofJvW)A2QBw z><06UG!FYa*H#A%#VGHu!JC%L%XwAH6<1bA)<0~> zI;-YMP;?wk)WOS;=-^m50u}4Ed6-ST46sS6gvAVFgO_yo{KrFr$M?EU0P%xi$V0&} zL?sxmx@|4gr*Vn0%~X2F+Pc@`$;p}Il$f3$OQyyq(lc|()PXE|95?B=nZ4fooh$nU zSAppjR^hTu^O942U*#*2!E$7<6gjU)&Q~HA%aMyOE?qB0Zs?I4RUhc?;|gBami^Z3 zire&um|dSW#^%4{)W!qI9j5`J1X7%3CIcmzeKCSjo2z1 z%pE&1H*RR?!S6CH+m6$UC2LOmppS=gT8^UMWCgvi z@UJ;>DfkIfE+F7Z!|<9HZNqO}u#JFyu~zzV_!`w(9SN&}aN!!W9R2G(&9rlLF2JDb zJ%G26@8^|)sq(-SlBrd1kJhOLt^fI{|$Sr+QxSJHxpIPXEwv^k3lR{IkpF$ z-Br$g9G;!kmevS7PgYO*Ja8znA?(Jc4J~$!%Ag4+MH^Z?sBA-9ld|6fo7IL+yH=NB z&0HU@DC(v+dL;~XbFJ5~hSxE={x|;1roTrNJdnUXCu$0y>Sh>bkF;&l-yR8U(qENb SVi>UC+i{=DUmwy-C;cyqnRa;q diff --git a/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc b/wikideck/Mine/__pycache__/Transaction.cpython-311.pyc index 073a521992339f46fffc9295c2cf30d4609be488..5ed8c6dd7539e5634d00198f7698ac014e53d53e 100644 GIT binary patch literal 9137 zcmdT}Yit`=cD}=xNO7o_X-cCvEm1NpiL~XX{D@=Jk2tnucV%x96$wjoMiwPL)H_4l zQng(*2(aG5ZN06ED1m@w3MI7~EwF$3uYU?8|3zU?5d#odpx7VzN5KvfKL&OACu$p2uWl5FM7^Lfa8Ohh7b2{KiB za#NgMwoF;{r*+B-PfNm7c}axWU2lTsp7IcB2rj3h(OBTF3dq z^PaW_T|Eah_Z=Ed$`>AT72B8UpyXzpPI_>OWcf|JC_b z=VP1dY|jsbi^S62uqvzy|M9MR^6JjXx3^EeUEsYc-=*^YU906(!wUlBvF%Ai(Lq|f zAmh&apgTO;dg0;83x}{Z?`|v-&NYzdn|An~ZN4XK%kcq~4{Y&)yt@P24nx~NUjMu6 zTfTuDKdAD9Ta|Co3avWJbC8$Bm{~%h0e-p*{WrM7#{d&cWEPq9lr?fzDv^V^j>60u zT!NV~A+yXS6oUyuPsVRH!5S{+9wDxOw^28-VbPpYDwf9mQgckFX)>z(V9zMiBWHrP zTAV@E-Pzy(r_xl3%t;G!(4twB_op?t9EIIEA*JpsGr;;{(!lPNll=Dbo7)O!2MxW?bsIj-t5Yu?Gs zy;&)ylqv!FuBWEcngh@*ElQe0U!z2E7t*Y^Cnq)A__eFYC@NlaOult#^yDe6e*DtA zk(*a0uHAV{b6t59ljd<_)tauS9zb{@PFBD3`ToP}<2(ItZ1=xW2#l(Mb86sXq3@{LcR}s@SC{n#jPBVzcw$&@KV;0?BtxWO(%7PyK#y zpnZ=V+@6X2~kYP@xRn@ ze2L3K#onZ_um(_&VKn$Tt2d%JASvT9dsz+FOTwNSu9sjx%);K_lmI=BOE;`!AcI+%2523czwvw zgN;6mA(5W;;Pvaw0QOJ~{UHoxI|m{+?S+AZZ4|AO9zimMWEcr<2KzxB4PgVC6AdFd zisTp)+yv-xBqxxdEzy%mP9Z_;qNkCZL4vC)BNN4)!iY_}Jc~`nXbs~@>?eN*WSQiJ z;EoX57D5I7U;*H$UI!jEcrF(>uLjN+{QcQG>mTI&CshB5LbpHLzdn@f4yoOtd~g4{ zXQ%h z&wAE-a^7RA_gJB`Z|#QKISMf3X@M!R0}y~A&w(f7zrOY4)~>&~9imDg7&ktK5ABAv zo4^9!0y_NjQqZ^md1;9gIotqB$!ySI5Da`wCm|eFPy3&mU`$JOOwno%Bw$nqH!qdR zR_(t>CI&-_!NIHG0<+tzz4wP2vF2=~Fc&1m?1Y;&$N6apUaF@xr8*G?%nY;?LwI9e zJ#8`aiO%W(rg2}kGFO%V|7B&>;J7*wuB=-B>Xk9Rnw7D71vZG@L8@VP?EkDPO%%G;jT|Swfr^ zif>S}-MxGLa%o(YTXRBO2jNpvG8|LUgtY_u!FFCsM#=#mgpnn9Lk}rMj`eXlo`S=2 zDkf=mB{QFpN~c@99t>)Acr2cg9+@Cd53f+RayUj2mlxZOkY_$p--LGgm>!Q6y8LVJ zWtC^Q)WP$)t_y0{1^t*>5d5leWJfr@EgXkv?P&*`PV;>KuHA|_0t9gc0H(ELC7!iq z-(7FW`NFC%4At&G`ubGgY3$YZWMYT+Z}a}_V2(ec@<$5ZzMMCpdIJDetqlNGtvFh1 zLlHs5cTjZ)F*pk5+q>6>veC8BrDnD!-U%>GN9ACik1svZ~89({?-JE+!br0eABl*tWwee?uwR0FI7%vFjzwP?8 z3zTw09Xyj0##CVp`Wj%KMTbodYCk4Mhyq7ooMywU;bO4)N-hSYW-gcow?jQ; z8B|tk@Tstv2%-lTZ&e}C)Xrk9>Unujq{M*BI7@A$1ah%?e`0d6XfVrw#<&tQq(j8# z0O+tyP(LtH9TOZ?7%{*F?g}NxY}_~E!GQI3A|5u5W(8N@;PfMxjHXmRZq-HQh!~G4 ziwBH8Uaq_ceDZ$+Stft%>dksp|M6Vc3AO7)LFin)v-Uww7*>Vhf=^iOUmME#22|fb z-q*e|x8n9rzj1&b=6S!%KxXR%P^B!0Y+PLm# zKZA_f&#GPqHwDa>fQ_Y^H_B~V%-bxyU+csj8h{Q#t08WE8!G5^ASUfcB7pF41!&TK zxsoDt50GV2Xgu&_akc-q!=Da+5?TxW{_Q}JD z%00bkeN3hyM*7G#1-VDulnu@Chc@H8znd!Ee#RFi@tc1Jy~QfS;rN)>bvF%}f8os9 zGwUX6)H(Lm(^Adt3S1>)V8e91WPuW!Q9ljfZQF)Tv&)%ziPA18X?1W7EJyE4T0LxV zN=E)OxSrUs>DEmrfq~vZE>|QHOGITE6pHU0E^U|fO9p+6MgPlI9je^HK7dZo@xbvt zyaK-Y*Vmq0+u=L6`OejMs<)#ZhXCns=DQEWn4g=`c@@DC3gg3q#IsvU2_&vV>1O%*n ziWaL2_PYNA(FLX-z%ViRu~c~puWLmn8!q4WeDn0DVnFoIVPW(xko|0-(`XB2*_jzE zVb6DvU262(Ti{yPE##dBV?n|T4@Yl!@L4y6L1ms64*d*ko1eK!V!qrS-I-W>K zqAt`=!c||b>3~twwToaX{}2_%R8B=K?yK;o+9BwD;Qd?VuWCLH#UG{lgC?QxMN=aD zB_)3I;!nf4yZ4oz>H0A$i|HDGD}fdJYTI?GvUWNLOus_Ls+vrD z*Diu7{U1|ClqQ@Y3 zVxwb75bubzI>AU?z5y0r*=XRp>(*rc=Ix@@V~4;0%%r=+ z4!6;aoNci~NXE!vul;P1Ff!bLt&H??*vg2*W{3T3FL}DKi?uJRP5fRaz2np1>vFD+ zy)Ng|>#xhXcT6U6X=6YY2YK2656u!!mx4z9KA3&ncGYGz#GZd8{^Q8#p z4f+s@U<^e59T50Gf#dR|aasT5iECN^ literal 8168 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