Unit testing basic workflows
This commit is contained in:
parent
1c31de2bb4
commit
05fc8f9a46
@ -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}'@'%';
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
|
||||
);
|
@ -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
|
||||
|
261
test/Mine/TestMine.py
Normal file
261
test/Mine/TestMine.py
Normal 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
10
test/test.py
Normal 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()
|
@ -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:
|
||||
|
@ -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'])
|
||||
|
@ -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 []
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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()
|
||||
|
@ -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))
|
Loading…
Reference in New Issue
Block a user