Unit testing basic workflows

This commit is contained in:
Eric Meehan 2025-06-04 11:47:49 -04:00
parent 1c31de2bb4
commit 05fc8f9a46
22 changed files with 477 additions and 223 deletions

View File

@ -1,51 +0,0 @@
CREATE DATABASE IF NOT EXISTS market;
USE market;
CREATE TABLE IF NOT EXISTS users(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
userId VARCHAR(36) UNIQUE NOT NULL,
userName VARCHAR(64) UNIQUE NOT NULL,
passwordHash VARCHAR(64) NOT NULL,
email VARCHAR(64) UNIQUE NOT NULL,
balance FLOAT NOT NULL DEFAULT 0,
publicKey VARCHAR(128) NOT NULL
);
CREATE TABLE IF NOT EXISTS buy_orders(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
buyOrderId VARCHAR(36) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
expires DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP() + INTERVAL 3 DAY,
pageId INT NOT NULL,
price FLOAT NOT NULL,
volume INT NOT NULL,
fee FLOAT NOT NULL DEFAULT 0,
sold INT NOT NULL DEFAULT 0,
FOREIGN KEY (userId) REFERENCES users(userId)
);
CREATE TABLE IF NOT EXISTS sell_orders(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
sellOrderId VARCHAR(36) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP(),
expires DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP() + INTERVAL 3 DAY,
price FLOAT NOT NULL,
fee FLOAT NOT NULL,
FOREIGN KEY (userId) REFERENCES users(userId)
);
CREATE TABLE IF NOT EXISTS sell_orders_items(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
cardId VARCHAR(36) NOT NULL,
FOREIGN KEY (sellOrderId) REFERENCES sell_orders(sellOrderId) NOT NULL
);
CREATE TABLE IF NOT EXISTS transactions(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
transactionId VARCHAR(36) UNIQUE NOT NULL,
isPending BOOLEAN NOT NULL DEFAULT true,
isAbandoned BOOLEAN NOT NULL DEFAULT false,
FOREIGN KEY (buyOrderId) REFERENCES buy_orders(buyOrderId) NOT NULL,
FOREIGN KEY (sellOrderItemSqlId) REFERENCES sell_order_items(sqlId) NOT NULL
);
CREATE USER IF NOT EXISTS '${MARKET_DB_USER}'@'%' IDENTIFIED BY '${MARKET_DB_PASSWORD}';
CREATE USER IF NOT EXISTS '${ORDER_MATCHER_DB_USER}'@'%' IDENTIFIED BY '${ORDER_MATCHER_DB_PASSWORD}';
CREATE USER IF NOT EXISTS '${STATUS_CHECKER_DB_USER}'@'%' IDENTIFIED BY '${STATUS_CHECKER_DB_PASSWORD}';
GRANT ALL PRIVILEGES ON market.* TO '${MARKET_DB_USER}'@'%';
GRANT ALL PRIVILEGES ON market.* TO '${ORDER_MATCHER_DB_USER}'@'%';
GRANT ALL PRIVILEGES ON market.* TO '${STATUS_CHECKER_DB_USER}'@'%';

View File

@ -1,12 +0,0 @@
SELECT buy_order_id
FROM buy_orders
JOIN cards
ON buy_orders.title = cards.title
WHERE title == desired_title
AND price >= desired_price
AND volume > (
SELECT count(*)
FROM transactions
WHERE buy_order_id == buy_order_id;
)
ORDER BY price DESC;

View File

@ -1,10 +0,0 @@
SELECT cardId, pageId, transactionId
FROM (
SELECT cardId, pageId, transactionId, ROW_NUMBER() OVER (
PARTITION BY cardId ORDER BY timestamp DESC
) AS rowNumber
FROM transactions
JOIN cards
WHERE receiver == {userId}
) AS subquery
WHERE rowNumber == 0;

View File

@ -1,11 +0,0 @@
SELECT sell_orders.sell_order_id, sell_order_items.card_id
FROM sell_orders
JOIN sell_order_items
ON sell_orders.sell_order_id = sell_order_items.sell_order_id
WHERE price <= desired_price
AND sell_order_items.card_id NOT IN (
SELECT card_id
FROM transactions
WHERE sell_order_id == sell_order_id;
)
ORDER BY price ASC;

View File

@ -1,38 +0,0 @@
CREATE TABLE IF NOT EXISTS blocks(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(32) UNIQUE NOT NULL,
blockHash VARCHAR(64) UNIQUE NOT NULL,
previousHash VARCHAR(64) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL,
height INT UNIQUE NOT NULL,
nonce INT NOT NULL
);
CREATE TABLE IF NOT EXISTS cards(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(32) UNIQUE NOT NULL,
cardId VARCHAR(32) UNIQUE NOT NULL,
pageId INT NOT NULL,
FOREIGN KEY (blockId) REFERENCES blocks(blockId)
);
CREATE TABLE IF NOT EXISTS transactions(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(32) NOT NULL,
cardId VARCHAR(32) UNIQUE NOT NULL,
transactionId VARCHAR(32) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL,
sender VARCHAR(128) NOT NULL,
receiver VARCHAR(128) NOT NULL,
signature VARCHAR(128) NOT NULL,
isPending BOOLEAN NOT NULL DEFAULT true,
isAbandoned BOOLEAN NOT NULL DEFAULT false,
FOREIGN KEY (blockId) REFERENCES blocks(blockId),
FOREIGN KEY (cardId) REFERENCES cards(cardId)
);
CREATE TABLE IF NOT EXISTS peers(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
peerId VARCHAR(32) UNIQUE NOT NULL,
baseUrl VARCHAR(128) UNIQUE NOT NULL,
isUp BOOLEAN NOT NULL,
downCount INT DEFAULT 0,
lastTry TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

View File

@ -1,15 +1,15 @@
version: '3.8'
services:
wikideck:
wikideck-mine:
image: ericomeehan/wikideck:latest
build:
context: .
dockerfile: Dockerfile
environment:
ROLE: api
ROLE: mine
DATA_PATH: /tmp
DIFFICULTY_REQUIREMENT: 3
DIFFICULTY_REQUIREMENT: 10
MINE_DB_HOST: mariadb-mine
MINE_DB_USER: mine
MINE_DB_PASSWORD: 123abc

View File

261
test/Mine/TestMine.py Normal file
View File

@ -0,0 +1,261 @@
import json
import os
import requests
import sys
import unittest
import uuid
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'wikideck')))
from Mine.Block import Block
from Mine.Transaction import Transaction
WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/')
class TestMine(unittest.TestCase):
def test_mine_block_send_card_mine_another_block(self):
# Create two users
privateKeyA = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyB = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
# Mine a block with privateKeyA
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockA = Block(
data = response.json()
)
newBlockA.mine(privateKeyA)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict())
self.assertEqual(response.status_code, 200)
# Send card from newBlockA to privateKeyB
newTransaction = Transaction(
cardId = newBlockA.card.cardId,
sender = privateKeyA.public_key(),
receiver = privateKeyB.public_key(),
authorPrivateKey = privateKeyA
)
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 200)
# Mine a block with privateKeyB
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockB = Block(
data = response.json()
)
newBlockB.mine(privateKeyB)
self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash)
self.assertTrue(newBlockA.timestamp < newBlockB.timestamp)
self.assertTrue(newBlockB.height == newBlockA.height + 1)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict())
self.assertEqual(response.status_code, 200)
# Validate decks
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyA.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyB.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId))
self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId)
self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId))
self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
def test_invalid_block_modified_transaction(self):
# Create two users
privateKeyA = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyB = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
# Mine a block with privateKeyA
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockA = Block(
data = response.json()
)
newBlockA.mine(privateKeyA)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict())
self.assertEqual(response.status_code, 200)
# Send card from newBlockA to privateKeyB
newTransaction = Transaction(
cardId = newBlockA.card.cardId,
sender = privateKeyA.public_key(),
receiver = privateKeyB.public_key(),
authorPrivateKey = privateKeyA
)
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 200)
# Mine a block with privateKeyB
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockB = Block(
data = response.json()
)
# Modify transactions
newBlockB.transactions[0].sender = privateKeyB.public_key()
newBlockB.transactions[0].receiver = privateKeyA.public_key()
newBlockB.mine(privateKeyB)
self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash)
self.assertTrue(newBlockA.timestamp < newBlockB.timestamp)
self.assertTrue(newBlockB.height == newBlockA.height + 1)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict())
self.assertEqual(response.status_code, 400)
# Validate decks
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyA.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId))
self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId)
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyB.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
# Mine a block with privateKeyB
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockB = Block(
data = response.json()
)
newBlockB.mine(privateKeyB)
self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash)
self.assertTrue(newBlockA.timestamp < newBlockB.timestamp)
self.assertTrue(newBlockB.height == newBlockA.height + 1)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict())
self.assertEqual(response.status_code, 200)
# Validate decks
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyA.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyB.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId))
self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId)
self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId))
self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
def test_invalid_transaction(self):
# Create three users
privateKeyA = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyB = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyC = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
# Mine a block with privateKeyA
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockA = Block(
data = response.json()
)
newBlockA.mine(privateKeyA)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockA.as_dict())
self.assertEqual(response.status_code, 200)
# Send card from newBlockA to privateKeyB
newTransaction = Transaction(
cardId = newBlockA.card.cardId,
sender = privateKeyA.public_key(),
receiver = privateKeyB.public_key(),
authorPrivateKey = privateKeyA
)
# Invalidate transaction
newTransaction.cardId = uuid.uuid4()
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 400)
newTransaction.cardId = newBlockA.card.cardId
newTransaction.sender = privateKeyC.public_key()
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 400)
newTransaction.sender = privateKeyA.public_key()
newTransaction.receiver = privateKeyC.public_key()
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 400)
newTransaction.receiver = privateKeyB.public_key()
newTransaction.sign(privateKeyC)
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 400)
newTransaction.sign(privateKeyA)
response = requests.post(f"{WIKIDECK_URL}/transactions", json=newTransaction.as_dict())
self.assertEqual(response.status_code, 200)
# Mine a block with privateKeyB
response = requests.get(f"{WIKIDECK_URL}/blocks")
self.assertEqual(response.status_code, 200)
newBlockB = Block(
data = response.json()
)
newBlockB.mine(privateKeyB)
self.assertEqual(newBlockA.blockHash.hexdigest(), newBlockB.previousHash)
self.assertTrue(newBlockA.timestamp < newBlockB.timestamp)
self.assertTrue(newBlockB.height == newBlockA.height + 1)
response = requests.post(f"{WIKIDECK_URL}/blocks", json=newBlockB.as_dict())
self.assertEqual(response.status_code, 200)
# Validate decks
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyA.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 0)
response = requests.get(f"{WIKIDECK_URL}/cards", params={
'publicKey': privateKeyB.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')
})
self.assertEqual(response.json()[0]['cardId'], str(newBlockA.card.cardId))
self.assertEqual(response.json()[0]['pageId'], newBlockA.card.pageId)
self.assertEqual(response.json()[1]['cardId'], str(newBlockB.card.cardId))
self.assertEqual(response.json()[1]['pageId'], newBlockB.card.pageId)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
if __name__ == '__main__':
unittest.main()

10
test/test.py Normal file
View File

@ -0,0 +1,10 @@
import os
import sys
import unittest
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from Mine.TestMine import TestMine
if __name__ == '__main__':
unittest.main()

View File

@ -14,6 +14,7 @@ class Block():
if data:
self.load_from_data(data)
else:
delay = 2
self.blockId = blockId if blockId else uuid.uuid4()
self.previousHash = previousHash
self.timestamp = timestamp if timestamp else datetime.datetime.now(
@ -22,7 +23,13 @@ class Block():
self.height = height
self.difficulty = difficulty
self.nonce = nonce
self.card = card if card else Card()
while True:
try:
self.card = card if card else Card()
except ReadTimeout as e:
time.sleep(delay := delay**2)
continue
break
self.transactions = transactions
self.update()
@ -51,19 +58,40 @@ class Block():
# respective schemas.
###
def validate(self):
# TODO: validate blockId is uuid
# TODO: validate previousHash is sha256 hash
if not isinstance(self.blockId, uuid.UUID):
raise TypeError(f"Block ID should be a UUID not {type(self.blockId).__name__}.")
if not self.blockId.version == 4:
raise self.Invalid(f"Block ID version should be 4 not {self.blockId.version}.")
if not isinstance(self.previousHash, str):
raise TypeError(
f"Previous hash should be string not {type(self.previousHash).__name__}."
)
if not int(self.blockHash.hexdigest(), 16) <= 2**(256-self.difficulty):
raise self.Invalid("Hash does not meet difficulty requirement.")
# TODO: validate timestamp is UTC timestamp
if not isinstance(self.timestamp, datetime.datetime):
raise TypeError(
f"Timestamp should be a datetime not {type(self.timestamp).__name__}"
)
if not self.timestamp.tzinfo == datetime.timezone.utc:
raise self.Invalid(
f"Timestamp timezone should be in UTC not {self.timestamp.tzinfo}."
)
if not self.timestamp < datetime.datetime.now(datetime.timezone.utc):
raise self.Invalid("Timestamp in the future.")
raise self.Invalid(f"Timestamp {self.timestamp} in the future.")
if not isinstance(self.height, int):
raise TypeError(f"Height should be integer not {type(self.height).__name__}.")
if not self.height >= 0:
raise self.Invalid("Height less than 0.")
raise self.Invalid(f"Height {self.height} less than 0.")
if not isinstance(self.difficulty, int):
raise TypeError(
f"Difficulty should be an integer not {type(self.difficulty).__name__}."
)
if not self.difficulty >= 0:
raise self.Invalid("Difficulty less than 0.")
raise self.Invalid(f"Difficulty {self.difficulty} less than 0.")
if not isinstance(self.nonce, int):
raise TypeError(f"Nonce should be an integer not {type(self.nonce).__name__}.")
if not self.nonce >= 0:
raise self.Invalid("Nonce less than 0.")
raise self.Invalid(f"Nonce {self.nonce} less than 0.")
self.card.validate()
seenTransactions = []
for transaction in self.transactions:

View File

@ -30,11 +30,17 @@ class Card():
self.load_from_data(data)
def validate(self):
if not isinstance(self.cardId, uuid.UUID):
raise TypeError(f"Card ID should be a UUID not {type(self.cardId).__name__}.")
if not self.cardId.version == 4:
raise self.Invalid(f"Card ID version should be 4 not {self.cardId.version}.")
if not isinstance(self.pageId, int):
raise TypeError(f"Page ID should be an integer not {type(self.pageId).__name__}.")
try:
# TODO: cardId is UUID
wikipedia.page(pageid=self.pageId)
# TODO: may need more precision here.
except Exception as e:
raise self.Invalid("Page ID does not match a Wikipedia page.")
raise self.Invalid(f"Page ID {self.pageId} does not match a Wikipedia page.")
def load_from_data(self, data):
self.cardId = uuid.UUID(data['cardId'])

View File

@ -105,6 +105,12 @@ class Database():
VALUES (?, ?, ?, ?, ?, ?, ?);
"""
SQL_GET_BLOCKS = """
SELECT blockId, previousHash, timestamp, height, difficulty, nonce
FROM mine.blocks
ORDER BY height ASC;
"""
SQL_GET_LAST_BLOCK = """
SELECT blockId, previousHash, timestamp, height, difficulty, nonce
FROM mine.blocks
@ -157,7 +163,21 @@ class Database():
SELECT transactionId, timestamp, cardId, sender, receiver, signature
FROM mine.transactions
WHERE blockId = ?
ORDER BY timestamp DESC;
ORDER BY timestamp ASC;
"""
SQL_GET_DECK = """
SELECT cards.cardId, cards.pageId
FROM (
SELECT transactionId, cardId, timestamp, receiver, ROW_NUMBER() OVER (
PARTITION BY transactions.cardId
ORDER BY transactions.timestamp DESC
) AS rownumber
FROM transactions
) AS subquery
JOIN cards ON cards.cardId = subquery.cardId
WHERE subquery.rownumber = 1
AND receiver = ?;
"""
def __init__(self):
@ -197,7 +217,7 @@ class Database():
transactions = blockTransactions
)
else:
return False
return None
def get_card_by_id(self, cardId):
cur = self.conn.cursor()
@ -206,7 +226,7 @@ class Database():
return Card(
cardId = uuid.UUID(card[0]),
pageId = card[1]
)
) if card else None
def get_card_by_block_id(self, blockId):
cur = self.conn.cursor()
@ -215,7 +235,32 @@ class Database():
return Card(
cardId = uuid.UUID(card[0]),
pageId = card[1]
)
) if card else None
def get_blocks(self):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_LAST_BLOCK)
blocks = cur.fetchall()
if blocks:
blockCard = self.get_card_by_block_id(lastBlock[0])
blockTransactions = self.get_transactions_by_block_id(lastBlock[0])
return [
Block(
blockId = uuid.UUID(block[0]),
previousHash = block[1],
timestamp = datetime.datetime.strptime(
block[2],
"%Y-%m-%d %H:%M:%S.%f%z"
),
height = block[3],
difficulty = block[4],
nonce = block[5],
card = blockCard,
transactions = blockTransactions
)
]
else:
return None
def get_last_block(self):
cur = self.conn.cursor()
@ -395,3 +440,17 @@ class Database():
transaction.signature.hex()
))
def get_deck(self, publicKey):
cur = self.conn.cursor()
cur.execute(
self.SQL_GET_DECK,
[publicKey.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')]
)
deck = cur.fetchall()
return [Card(
cardId = card[0],
pageId = card[1]
) for card in deck] if deck else []

View File

@ -4,6 +4,8 @@ import json
import os
import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from Mine.Block import Block
@ -11,7 +13,7 @@ from Mine.Card import Card
from Mine.Database import Database
from Mine.Transaction import Transaction
BLOCK_TRANSACTION_LIMIT = int(os.getenv('BLOCK_TRANSACTION_LIMIT', 32768))
BLOCK_TRANSACTION_LIMIT = int(os.getenv('BLOCK_TRANSACTION_LIMIT', 1024))
DATA_PATH = os.getenv('DATA_PATH', '/var/lib/wikideck/blocks')
DIFFICULTY_REQUIREMENT = int(os.getenv('DIFFICULTY_REQUIREMENT', 0))
@ -23,16 +25,16 @@ privateKey = rsa.generate_private_key(
)
def save_block(block):
# Blocks are json strings. This is the true block.
with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f:
f.write(str(block))
# TODO: error handling (don't reveal mariadb errors)
# TODO: disable autocommit
db.insert_block(block)
db.insert_card(block.blockId, block.card)
for transaction in block.transactions:
db.delete_pending_transaction(transaction.transactionId)
db.insert_transaction(block.blockId, transaction)
# TODO: delete block and card if inserts fail
# Blocks are json strings. This is the true block.
with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f:
f.write(str(block))
# TODO update peers
def generate_origin_block():
@ -47,8 +49,14 @@ def generate_origin_block():
@mine.get('/')
def index_get():
# TODO: return a page to mine through the browser
return "Hello world!", 200
try:
return jsonify([each.as_dict() for each in db.get_blocks()])
except Exception as e:
return flask.jsonify(
{'Error': str(e)}
), e.statusCode if hasattr(
e, 'statusCode'
) else 500
###
# Retrieve blocks and block data.
@ -86,7 +94,7 @@ def blocks_post():
previousBlock = db.get_last_block()
if newBlock.previousHash != previousBlock.blockHash.hexdigest():
raise Block.Invalid(
f"Incorrect previous hash - should be {previousBlock.blockHash.hexdigest()}."
f"Previous hash should be {previousBlock.blockHash.hexdigest()}."
)
if newBlock.timestamp <= previousBlock.timestamp:
raise Block.Invalid(
@ -94,11 +102,11 @@ def blocks_post():
)
if newBlock.height != previousBlock.height + 1:
raise Block.Invalid(
f"Incorrect block height - should be {previousBlock.height + 1}."
f"Height should be {previousBlock.height + 1} not {newBlock.height}."
)
if newBlock.difficulty < DIFFICULTY_REQUIREMENT:
raise Block.Invalid(
f"Incorrect difficulty - should be {DIFFICULTY_REQUIREMENT}."
f"Difficulty should be {DIFFICULTY_REQUIREMENT} not {newBlock.difficulty}."
)
if len(newBlock.transactions) == 0:
raise Block.Invalid(
@ -151,10 +159,25 @@ def blocks_post():
###
@mine.get('/cards')
def cards_get():
# TODO: render cards in html
# TODO: query cards
# TODO: get decks
return 200
try:
# TODO: render cards in html
# TODO: query cards
publicKeyString = flask.request.args.get('publicKey', None)
if publicKeyString:
deck = db.get_deck(
serialization.load_pem_public_key(
publicKeyString.encode('utf-8'),
backend=default_backend()
)
)
return flask.jsonify([card.as_dict() for card in deck] if deck else [])
except Exception as e:
return flask.jsonify(
{'Error': str(e)}
), e.statusCode if hasattr(
e, 'statusCode'
) else 500
###
# Submit a transaction to be mined in a block.

View File

@ -6,7 +6,7 @@ import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric import padding, rsa
class Transaction():
def __init__(self, transactionId=None, timestamp=None, cardId=None, sender=None,
@ -25,11 +25,45 @@ class Transaction():
self.sign(authorPrivateKey)
def validate(self):
# TODO: validate transactionId
# TODO: validate timestamp
# TODO: validate cardId
# TODO: validate sender
# TODO: validate receiver
# TODO: better error codes
if not isinstance(self.transactionId, uuid.UUID):
raise self.Invalid(
f"Transaction ID should be a UUID not {type(self.transactionId).__name__}."
)
if not self.transactionId.version == 4:
raise self.Invalid(
f"Transaction ID version should be 4 not {self.transactionId.version}."
)
if not isinstance(self.timestamp, datetime.datetime):
raise self.Invalid(
f"Timestamp should be a datetime not {type(self.timestamp).__name__}"
)
if not self.timestamp.tzinfo == datetime.timezone.utc:
raise self.Invalid(
f"Timestamp timezone should be in UTC not {self.timestamp.tzinfo}."
)
if not self.timestamp < datetime.datetime.now(datetime.timezone.utc):
raise self.Invalid(f"Timestamp {self.timestamp} in the future.")
if not isinstance(self.cardId, uuid.UUID):
raise self.Invalid(
f"Card ID should be a UUID not {type(self.cardId).__name__}."
)
if not self.cardId.version == 4:
raise self.Invalid(
f"Card ID version should be 4 not {self.cardId.version}."
)
if not isinstance(self.sender, rsa.RSAPublicKey):
raise self.Invalid(
f"Sender should be an RSA public key not {type(self.sender).__name__}."
)
if not isinstance(self.receiver, rsa.RSAPublicKey):
raise self.Invalid(
f"Receiver should be an RSA public key not {type(self.receiver).__name__}."
)
if not isinstance(self.signature, bytes):
raise self.Invalid(
f"Signature should be bytes not {type(self.signature).__name__}."
)
try:
self.sender.verify(
self.signature,
@ -38,6 +72,10 @@ class Transaction():
"transactionId": str(self.transactionId),
"timestamp": str(self.timestamp),
"cardId": str(self.cardId),
"sender": self.sender.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
"receiver": self.receiver.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
@ -50,6 +88,7 @@ class Transaction():
),
hashes.SHA256()
)
# TODO: may need more precision.
except Exception as e:
raise self.InvalidSignature(str(e))
@ -60,6 +99,10 @@ class Transaction():
"transactionId": str(self.transactionId),
"timestamp": str(self.timestamp),
"cardId": str(self.cardId),
"sender": self.sender.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
"receiver": self.receiver.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
@ -80,7 +123,7 @@ class Transaction():
"%Y-%m-%d %H:%M:%S.%f%z"
)
self.cardId = uuid.UUID(data['cardId'])
# TODO: why is this sometimes a tuple?
# TODO: why is this a tuple when coming from POST /transactions?
self.sender = serialization.load_pem_public_key(
data['sender'].encode('utf-8'),
backend=default_backend()
@ -132,12 +175,6 @@ class Transaction():
self.message = message
self.statusCode = statusCode
class Abandoned(Exception):
def __init__(self, message, statusCode=500):
super().__init__(message)
self.message = message
self.statusCode = statusCode
class InvalidSignature(Exception):
def __init__(self, message, statusCode=400):
super().__init__(message)

View File

@ -1,13 +1,17 @@
import dotenv
import flask
import time
import os
ROLE = os.getenv('ROLE', 'mine')
if ROLE == 'mine':
from Mine.Mine import mine
app = flask.Flask(__name__)
app.register_blueprint(mine)
#from Market.Market import market
from Mine.Mine import mine
app = flask.Flask(__name__)
#app.register_blueprint(market, url_prefix="/market")
app.register_blueprint(mine, url_prefix="/mine")
else:
raise Exception("Environment variable ROLE must be either 'mine' or 'market'.")
if __name__ == "__main__":
dotenv.load_dotenv()

View File

@ -1,52 +0,0 @@
import json
import os
import requests
from cryptography.hazmat.primitives.asymmetric import rsa
from Mine.Block import Block
from Mine.Transaction import Transaction
WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/')
print("Generating RSA keys...")
privateKeyA = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyB = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
print("Getting block to mine from server...")
newBlockA = Block(
data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json()
)
print("Received block for user A to mine.")
print("Mining block...")
newBlockA.mine(privateKeyA)
r = requests.post(f"{WIKIDECK_URL}/mine/blocks", json=newBlockA.as_dict())
print(json.dumps(r.json(), indent=4))
input("Press enter to continue...")
print("Sending card A to user B...")
newTransaction = Transaction(
cardId = newBlockA.card.cardId,
sender = privateKeyA.public_key(),
receiver = privateKeyB.public_key(),
authorPrivateKey = privateKeyA
)
r = requests.post(f"{WIKIDECK_URL}/mine/transactions", json=newTransaction.as_dict())
print(json.dumps(r.json(), indent=4))
input("Press enter to continue...")
print("Getting block to mine from server...")
newBlockB = Block(
data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json()
)
print("Received block for user B to mine.")
print("Mining block...")
newBlockB.mine(privateKeyB)
r = requests.post(f"{WIKIDECK_URL}/mine/blocks", json=newBlockB.as_dict())
print(json.dumps(r.json(), indent=4))