Functional prototype

This commit is contained in:
Eric Meehan 2025-06-03 11:20:07 -04:00
parent 741b1f5b2a
commit 1c31de2bb4
11 changed files with 476 additions and 201 deletions

View File

@ -8,7 +8,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
ROLE: api ROLE: api
DATA_PATH: /data DATA_PATH: /tmp
DIFFICULTY_REQUIREMENT: 3 DIFFICULTY_REQUIREMENT: 3
MINE_DB_HOST: mariadb-mine MINE_DB_HOST: mariadb-mine
MINE_DB_USER: mine MINE_DB_USER: mine
@ -17,8 +17,6 @@ services:
- mariadb-mine - mariadb-mine
ports: ports:
- "8080:80" - "8080:80"
volumes:
- ./mine_data:/data
mariadb-mine: mariadb-mine:
image: mariadb:latest image: mariadb:latest
@ -29,6 +27,3 @@ services:
MARIADB_PASSWORD: 123abc MARIADB_PASSWORD: 123abc
ports: ports:
- "3306:3306" - "3306:3306"
volumes:
mine_data:

View File

@ -3,23 +3,26 @@ import hashlib
import json import json
import uuid import uuid
from cryptography.hazmat.primitives import serialization
from Mine.Card import Card from Mine.Card import Card
from Mine.Transaction import Transaction from Mine.Transaction import Transaction
class Block(): class Block():
def __init__(self, blockId=uuid.uuid4(), previousHash="0", def __init__(self, blockId=None, previousHash="0", timestamp=None, height=0, nonce=0,
timestamp=datetime.datetime.now(datetime.timezone.utc), height=0, nonce=0, difficulty=0, card=None, transactions=[], data=None):
difficulty=0, card=Card(), transactions=[], data=None):
if data: if data:
self.load_from_data(data) self.load_from_data(data)
else: else:
self.blockId = blockId self.blockId = blockId if blockId else uuid.uuid4()
self.previousHash = previousHash self.previousHash = previousHash
self.timestamp = timestamp self.timestamp = timestamp if timestamp else datetime.datetime.now(
datetime.timezone.utc
)
self.height = height self.height = height
self.difficulty = difficulty self.difficulty = difficulty
self.nonce = nonce self.nonce = nonce
self.card = card self.card = card if card else Card()
self.transactions = transactions self.transactions = transactions
self.update() self.update()
@ -62,8 +65,14 @@ class Block():
if not self.nonce >= 0: if not self.nonce >= 0:
raise self.Invalid("Nonce less than 0.") raise self.Invalid("Nonce less than 0.")
self.card.validate() self.card.validate()
seenTransactions = []
for transaction in self.transactions: for transaction in self.transactions:
if transaction in seenTransactions:
raise self.Invalid(
f"Contains duplicate transaction {transaction.transactionId}."
)
transaction.validate() transaction.validate()
seenTransactions.append(transaction)
# TODO validate that one transaction gives the card to the author # TODO validate that one transaction gives the card to the author
def load_from_data(self, data): def load_from_data(self, data):
@ -85,12 +94,12 @@ class Block():
transactionId = uuid.UUID(each['transactionId']), transactionId = uuid.UUID(each['transactionId']),
timestamp = datetime.datetime.strptime( timestamp = datetime.datetime.strptime(
each['timestamp'], each['timestamp'],
"%Y-%m-%d %H:%M:%S.%f" "%Y-%m-%d %H:%M:%S.%f%z"
), ),
cardId = uuid.UUID(each['cardId']), cardId = uuid.UUID(each['cardId']),
sender = each['sender'], sender = serialization.load_pem_public_key(each['sender'].encode('utf-8')),
receiver = each['receiver'], receiver = serialization.load_pem_public_key(each['receiver'].encode('utf-8')),
signature = each['signature'] signature = bytes.fromhex(each['signature'])
) for each in data['transactions'] ) for each in data['transactions']
] ]
self.update() self.update()
@ -110,9 +119,10 @@ class Block():
def __str__(self): def __str__(self):
# The hash of the block is the SHA256 hash of what this method returns. # 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): class Invalid(Exception):
def __init__(self, message, status_code=406): def __init__(self, message, statusCode=406):
super().__init__(message) super().__init__(message)
self.status_code = status_code self.message = message
self.statusCode = statusCode

View File

@ -1,11 +1,13 @@
import requests
import json import json
import wikipedia import requests
import time
import uuid import uuid
import wikipedia
class Card(): class Card():
def __init__(self, cardId=uuid.uuid4(), pageId=None, data=None): def __init__(self, cardId=None, pageId=None, data=None):
self.cardId = cardId self.cardId = cardId if cardId else uuid.uuid4()
delay = 2
while True: while True:
try: try:
self.pageId = pageId if pageId else int( self.pageId = pageId if pageId else int(
@ -19,6 +21,10 @@ class Card():
except wikipedia.exceptions.DisambiguationError as e: except wikipedia.exceptions.DisambiguationError as e:
# TODO pick random disambiuation option # TODO pick random disambiuation option
continue continue
except wikipedia.exceptions.WikipediaException as e:
time.sleep(delay)
delay = delay**2
continue
break break
if data: if data:
self.load_from_data(data) self.load_from_data(data)
@ -41,9 +47,10 @@ class Card():
} }
def __str__(self): def __str__(self):
return json.dumps(self.as_dict()) return json.dumps(self.as_dict(), sort_keys=True)
class Invalid(Exception): class Invalid(Exception):
def __init__(self, message, status_code=406): def __init__(self, message, statusCode=406):
super().__init__(message) super().__init__(message)
self.status_code = status_code self.message = message
self.statusCode = statusCode

View File

@ -1,44 +1,61 @@
import base64
import datetime
import mariadb import mariadb
import os import os
import time import time
import uuid
from cryptography.hazmat.primitives import serialization
from Mine.Block import Block from Mine.Block import Block
from Mine.Card import Card
from Mine.Transaction import Transaction from Mine.Transaction import Transaction
class Database(): class Database():
SQL_CREATE_DATABASE = [ SQL_CREATE_TABLES = [
""" """
CREATE TABLE IF NOT EXISTS mine.blocks( CREATE TABLE IF NOT EXISTS mine.blocks(
sqlId INT PRIMARY KEY AUTO_INCREMENT, sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) UNIQUE NOT NULL, blockId UUID UNIQUE NOT NULL,
blockHash VARCHAR(64) UNIQUE NOT NULL, blockHash VARCHAR(64) UNIQUE NOT NULL,
previousHash 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, height INT UNIQUE NOT NULL,
difficulty INT NOT NULL,
nonce INT NOT NULL nonce INT NOT NULL
); );
""", """,
""" """
CREATE TABLE IF NOT EXISTS mine.cards( CREATE TABLE IF NOT EXISTS mine.cards(
sqlId INT PRIMARY KEY AUTO_INCREMENT, sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) UNIQUE NOT NULL, blockId UUID UNIQUE NOT NULL,
cardId VARCHAR(37) UNIQUE NOT NULL, cardId UUID UNIQUE NOT NULL,
pageId INT NOT NULL, pageId INT NOT NULL,
FOREIGN KEY (blockId) REFERENCES blocks(blockId) 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( CREATE TABLE IF NOT EXISTS mine.transactions(
sqlId INT PRIMARY KEY AUTO_INCREMENT, sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) NOT NULL, blockId UUID NOT NULL,
cardId VARCHAR(37) UNIQUE NOT NULL, transactionId UUID UNIQUE NOT NULL,
transactionId VARCHAR(37) UNIQUE NOT NULL, cardId UUID NOT NULL,
timestamp DATETIME NOT NULL, timestamp VARCHAR(32) NOT NULL,
sender VARCHAR(128) NOT NULL, sender TEXT NOT NULL,
receiver VARCHAR(128) NOT NULL, receiver TEXT NOT NULL,
signature VARCHAR(128) NOT NULL, signature TEXT NOT NULL,
isPending BOOLEAN NOT NULL DEFAULT true,
isAbandoned BOOLEAN NOT NULL DEFAULT false,
FOREIGN KEY (blockId) REFERENCES blocks(blockId), FOREIGN KEY (blockId) REFERENCES blocks(blockId),
FOREIGN KEY (cardId) REFERENCES cards(cardId) FOREIGN KEY (cardId) REFERENCES cards(cardId)
); );
@ -46,7 +63,7 @@ class Database():
""" """
CREATE TABLE IF NOT EXISTS mine.peers( CREATE TABLE IF NOT EXISTS mine.peers(
sqlId INT PRIMARY KEY AUTO_INCREMENT, sqlId INT PRIMARY KEY AUTO_INCREMENT,
peerId VARCHAR(37) UNIQUE NOT NULL, peerId UUID UNIQUE NOT NULL,
baseUrl VARCHAR(128) UNIQUE NOT NULL, baseUrl VARCHAR(128) UNIQUE NOT NULL,
isUp BOOLEAN NOT NULL, isUp BOOLEAN NOT NULL,
downCount INT DEFAULT 0, downCount INT DEFAULT 0,
@ -58,61 +75,89 @@ class Database():
SQL_GET_BLOCK_BY_ID = """ SQL_GET_BLOCK_BY_ID = """
SELECT * SELECT *
FROM blocks FROM blocks
WHERE blockId = '{}'; WHERE blockId = ?;
""" """
SQL_GET_LAST_BLOCK = """ SQL_GET_CARD_BY_ID = """
SELECT * SELECT cardId, pageId
FROM blocks
ORDER BY timestamp DESC
LIMIT 1;
"""
SQL_GET_CARD = """
SELECT *
FROM cards FROM cards
WHERE cardId = '{}'; WHERE cardId = ?;
"""
SQL_GET_CARD_BY_BLOCK_ID = """
SELECT cardId, pageId
FROM cards
WHERE blockId = ?;
""" """
SQL_GET_CARD_OWNER = """ SQL_GET_CARD_OWNER = """
SELECT receiver SELECT receiver
FROM transactions FROM transactions
WHERE cardId = '{}' WHERE cardId = ?
AND isPending = False
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT 1; 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 = """ SQL_INSERT_BLOCK = """
INSERT INTO blocks (blockId, blockHash, previousHash, timestamp, height, nonce) INSERT INTO mine.blocks (
VALUES ('{}', '{}', '{}', '{}', {}, {}); 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 = """ SQL_INSERT_TRANSACTION = """
INSERT INTO transactions ( INSERT INTO mine.transactions (
transactionId, blockId, transactionId, timestamp, cardId, sender, receiver, signature
timestamp,
sender,
receiver,
signature,
blockId,
cardId
) )
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): def __init__(self):
@ -124,67 +169,229 @@ class Database():
port = os.getenv('MINE_DB_PORT', 3306), port = os.getenv('MINE_DB_PORT', 3306),
user = os.getenv('MINE_DB_USER', None), user = os.getenv('MINE_DB_USER', None),
password = os.getenv('MINE_DB_PASSWORD', None), password = os.getenv('MINE_DB_PASSWORD', None),
database = 'mine' database = 'mine',
autocommit = True
) )
for each in self.SQL_CREATE_DATABASE: for each in self.SQL_CREATE_TABLES:
cursor = self.conn.cursor().execute(each) self.conn.cursor().execute(each)
except mariadb.Error as e: except mariadb.Error as e:
time.sleep(delay := delay**2) time.sleep(delay := delay**2)
continue continue
break break
def get_block_by_id(self, blockId): def get_block_by_id(self, 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( return Block(
data=self.conn.cursor().execute( blockId = uuid.UUID(block[1]),
self.SQL_GET_BLOCK_BY_ID.format(blockId) 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): def get_last_block(self):
data=self.conn.cursor().execute(self.SQL_GET_LAST_BLOCK) cur = self.conn.cursor()
return Block(data=data) if data else None cur.execute(self.SQL_GET_LAST_BLOCK)
lastBlock = cur.fetchone()
def get_card(self, cardId): if lastBlock:
return Card( lastBlockCard = self.get_card_by_block_id(lastBlock[0])
data=self.conn.cursor().execute(self.SQL_GET_CARD.format(cardId)) 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): def get_card_owner(self, cardId):
return self.conn.cursor().execute( cur = self.conn.cursor()
self.SQL_GET_CARD_OWNER.format(cardId) 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): def get_pending_transactions(self, limit=32768):
pendingTransactions = self.conn.cursor().execute( cur = self.conn.cursor()
self.SQL_GET_PENDING_TRANSACTIONS.format(limit) cur.execute(
self.SQL_GET_PENDING_TRANSACTIONS,
[limit]
) )
pendingTransactions = cur.fetchall()
return [ return [
Transaction( Transaction(
data=each transactionId = uuid.UUID(pendingTransaction[0]),
) for each in pendingTransactions 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 [] ] if pendingTransactions else []
def get_transaction(self, 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( return Transaction(
data=self.conn.cursor().execute(self.SQL_GET_TRANSACTION.format(transactionId)) 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): 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), str(block.blockId),
block.blockHash.hexdigest(), block.blockHash.hexdigest(),
block.previousHash, block.previousHash,
str(block.timestamp), str(block.timestamp),
block.height, block.height,
block.difficulty,
block.nonce block.nonce
)) ))
def insert_transaction(self, transaction): def insert_card(self, blockId, card):
return self.conn.cursor().execute(self.SQL_INSERT_TRANSACTION.format( 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.transactionId),
str(transaction.timestamp), str(transaction.timestamp),
transaction.sender, str(transaction.cardId),
transaction.receiver, transaction.sender.public_bytes(
transaction.signature, encoding=serialization.Encoding.PEM,
str(transaction.cardId) 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()
))

View File

@ -1,6 +1,8 @@
import datetime
import flask import flask
import json import json
import os import os
import uuid
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
@ -21,19 +23,27 @@ privateKey = rsa.generate_private_key(
) )
def save_block(block): def save_block(block):
# Blocks are json strings. This is the true block.
with open(f"{DATA_PATH}/{block.blockId}.json", 'w') as f: 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_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(): def generate_origin_block():
# TODO: load blocks from files
# TODO: try to get blocks from peers
originBlock = Block( originBlock = Block(
difficulty = DIFFICULTY_REQUIREMENT, difficulty = DIFFICULTY_REQUIREMENT,
card = Card(pageId=18618509) # You can target specific cards
) )
originBlock.mine(privateKey) originBlock.mine(privateKey)
originBlock.validate() originBlock.validate()
save_block(originBlock) save_block(originBlock)
return originBlock
@mine.get('/') @mine.get('/')
def index_get(): def index_get():
@ -42,6 +52,7 @@ def index_get():
### ###
# Retrieve blocks and block data. # Retrieve blocks and block data.
# Creates an origin block if none exists
# Returns a skeleton block to be mined by default. # Returns a skeleton block to be mined by default.
# Queries for a specific block when given parameters. # Queries for a specific block when given parameters.
### ###
@ -49,17 +60,16 @@ def index_get():
def blocks_get(): def blocks_get():
# TODO: block queries # TODO: block queries
blockId = flask.request.args.get('blockId', None) 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( return flask.jsonify(
Block( Block(
previousHash = lastBlock.blockHash.hexdigest(), previousHash = lastBlock.blockHash.hexdigest(),
height = lastBlock.height + 1, height = lastBlock.height + 1,
difficulty = DIFFICULTY_REQUIREMENT, difficulty = DIFFICULTY_REQUIREMENT,
transactions = [ transactions = db.get_pending_transactions()
Transaction(data=each) for each in db.get_pending_transactions(
BLOCK_TRANSACTION_LIMIT
)
]
).as_dict() ).as_dict()
) )
@ -71,16 +81,16 @@ def blocks_get():
@mine.post('/blocks') @mine.post('/blocks')
def blocks_post(): def blocks_post():
try: try:
newBlock = Block(data=request.get_json()) newBlock = Block(data=flask.request.get_json())
newBlock.validate() newBlock.validate()
previousBlock = db.get_last_block() previousBlock = db.get_last_block()
if newBlock.previousHash != previousBlock.blockHash: if newBlock.previousHash != previousBlock.blockHash.hexdigest():
raise Block.Invalid( 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: if newBlock.timestamp <= previousBlock.timestamp:
raise Block.Invalid( raise Block.Invalid(
"Timestamp is later than previous block." f"Timestamp {newBlock.timestamp} is before {previousBlock.timestamp}."
) )
if newBlock.height != previousBlock.height + 1: if newBlock.height != previousBlock.height + 1:
raise Block.Invalid( raise Block.Invalid(
@ -95,15 +105,18 @@ def blocks_post():
"Block contains no transactions." "Block contains no transactions."
) )
for transaction in newBlock.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: if not pendingTransaction:
if transaction.cardId != newBlock.card.cardId:
raise Transaction.Invalid( raise Transaction.Invalid(
f"No matching pending transaction for {transaction.transactionId}." f"No matching pending transaction for {transaction.transactionId}."
) )
if not pendingTransaction.pending: else:
raise Transaction.AlreadyFulfilled(
f"Transaction {transaction.transactionId} has already been fulfilled."
)
if transaction.timestamp != pendingTransaction.timestamp: if transaction.timestamp != pendingTransaction.timestamp:
raise Transaction.Invalid( raise Transaction.Invalid(
f"Incorrect timestamp on {transaction.transactionId}." f"Incorrect timestamp on {transaction.transactionId}."
@ -116,27 +129,22 @@ def blocks_post():
raise Transaction.Invalid( raise Transaction.Invalid(
f"Incorrect sender on {transaction.transactionId}." f"Incorrect sender on {transaction.transactionId}."
) )
if transaction.recipient != pendingTransaction.recipient: if transaction.receiver != pendingTransaction.receiver:
raise Transaction.Invalid( raise Transaction.Invalid(
f"Incorrect recipient on {transaction.transactionId}." f"Incorrect receiver on {transaction.transactionId}."
) )
if transaction.signature != pendingTransaction.signature: if transaction.signature != pendingTransaction.signature:
raise Transaction.Invalid( raise Transaction.Invalid(
f"Incorrect signature on {transaction.transactionId}." f"Incorrect signature on {transaction.transactionId}."
) )
save_block(block) save_block(newBlock)
for transaction in newBlock.transactions: return flask.jsonify(newBlock.as_dict())
db.update_transactions_is_pending_false(transaction.transactionId) except Exception as e:
# TODO: update peers return flask.jsonify(
return flask.jsonify(block.asDict()) {'Error': str(e)}
except Transaction.Invalid as e: ), e.statusCode if hasattr(
return e, e.statusCode e, 'statusCode'
except Transaction.AlreadyFulfilled as e: ) else 500
return e, e.statusCode
except Card.Invalid as e:
return e, e.statusCode
except Block.Invalid as e:
return e, e.statusCode
### ###
# Retrieve card data # Retrieve card data
@ -153,29 +161,33 @@ def cards_get():
# This method performs a number of validations on the submitted transaction and returns # This method performs a number of validations on the submitted transaction and returns
# a status code result. # a status code result.
### ###
@mine.put('/transactions') @mine.post('/transactions')
def transactions_put(): def transactions_post():
try: try:
newTransaction = Transaction(data=request.get_json()) newTransaction = Transaction(data=flask.request.get_json())
newTransaction.validate() newTransaction.validate()
if not get_card(newTransaction.cardId): if not db.get_card_by_id(newTransaction.cardId):
raise Transaction.Invalid( raise Transaction.Invalid(
f"Card {newTransaction.cardId} does not exist.", f"Card {newTransaction.cardId} does not exist.",
404 404
) )
if newTransaction.sender != get_card_owner(newTransaction.cardId): if newTransaction.sender != db.get_card_owner(newTransaction.cardId):
raise Transaction.Unauthorized( raise Transaction.Unauthorized(
f"{newTransaction.sender} does not own {newTransaction.cardId}." 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? # TODO: update peers?
return 200 return flask.jsonify(newTransaction.as_dict())
except mariadb.Error as e: except Exception as e:
return e, 500 return flask.jsonify(
except Transaction.Unauthorized as e: {'Error': str(e)}
return e, e.statusCode ), e.statusCode if hasattr(
except Transaction.Invalid as e: e, 'statusCode'
return e, e.statusCode ) else 500
### ###
# Retrieve a transaction. # Retrieve a transaction.

View File

@ -3,17 +3,18 @@ import io
import json import json
import uuid import uuid
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
class Transaction(): class Transaction():
def __init__(self, transactionId=uuid.uuid4(), def __init__(self, transactionId=None, timestamp=None, cardId=None, sender=None,
timestamp=datetime.datetime.now(datetime.timezone.utc), receiver=None, authorPrivateKey=None, signature=None, data=None):
cardId=None, sender=None, receiver=None, authorPrivateKey=None, signature=None, self.transactionId = transactionId if transactionId else uuid.uuid4()
data=None): self.timestamp = timestamp if timestamp else datetime.datetime.now(
self.transactionId = transactionId datetime.timezone.utc
self.timestamp = timestamp )
self.cardId = cardId self.cardId = cardId
self.sender = sender self.sender = sender
self.receiver = receiver self.receiver = receiver
@ -29,7 +30,6 @@ class Transaction():
# TODO: validate cardId # TODO: validate cardId
# TODO: validate sender # TODO: validate sender
# TODO: validate receiver # TODO: validate receiver
# TODO: validate signature
try: try:
self.sender.verify( self.sender.verify(
self.signature, self.signature,
@ -42,7 +42,7 @@ class Transaction():
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8') ).decode('utf-8')
}).encode('utf-8') }, sort_keys=True).encode('utf-8')
), ),
padding.PSS( padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
@ -51,7 +51,7 @@ class Transaction():
hashes.SHA256() hashes.SHA256()
) )
except Exception as e: except Exception as e:
raise self.InvalidSignature("Invalid signature.") raise self.InvalidSignature(str(e))
def sign(self, authorPrivateKey): def sign(self, authorPrivateKey):
self.signature = authorPrivateKey.sign( self.signature = authorPrivateKey.sign(
@ -64,7 +64,7 @@ class Transaction():
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8') ).decode('utf-8')
}).encode('utf-8') }, sort_keys=True).encode('utf-8')
), ),
padding.PSS( padding.PSS(
mgf=padding.MGF1(hashes.SHA256()), mgf=padding.MGF1(hashes.SHA256()),
@ -80,21 +80,31 @@ class Transaction():
"%Y-%m-%d %H:%M:%S.%f%z" "%Y-%m-%d %H:%M:%S.%f%z"
) )
self.cardId = uuid.UUID(data['cardId']) self.cardId = uuid.UUID(data['cardId'])
# TODO: load rsa keys # TODO: why is this sometimes a tuple?
self.sender = data['sender'] self.sender = serialization.load_pem_public_key(
self.receiver = data['receiver'] data['sender'].encode('utf-8'),
self.signature = data['signature'] 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): def as_dict(self):
return { return {
"transactionId": str(self.transactionId), "transactionId": str(self.transactionId),
"timestamp": str(self.timestamp), "timestamp": str(self.timestamp),
"cardId": str(self.cardId), "cardId": str(self.cardId),
"receiver": self.receiver.public_bytes( "sender": self.sender.public_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'), ).decode('utf-8'),
"sender": self.sender.public_bytes( "receiver": self.receiver.public_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'), ).decode('utf-8'),
@ -102,29 +112,34 @@ class Transaction():
} }
def __str__(self): def __str__(self):
return json.dumps(self.as_dict()) return json.dumps(self.as_dict(), sort_keys=True)
class Unauthorized(Exception): class Unauthorized(Exception):
def __init__(self, message, statusCode=403): def __init__(self, message, statusCode=403):
super().__init__(message) super().__init__(message)
self.message = message
self.statusCode = statusCode self.statusCode = statusCode
class Invalid(Exception): class Invalid(Exception):
def __init__(self, message, statusCode=400): def __init__(self, message, statusCode=400):
super().__init__(message) super().__init__(message)
self.message = message
self.statusCode = statusCode self.statusCode = statusCode
class AlreadyFulfilled(Exception): class AlreadyFulfilled(Exception):
def __init__(self, message, statusCode=400): def __init__(self, message, statusCode=400):
super().__init__(message) super().__init__(message)
self.message = message
self.statusCode = statusCode self.statusCode = statusCode
class Abandoned(Exception): class Abandoned(Exception):
def __init__(self, message, statusCode=500): def __init__(self, message, statusCode=500):
super().__init__(message) super().__init__(message)
self.message = message
self.statusCode = statusCode self.statusCode = statusCode
class InvalidSignature(Exception): class InvalidSignature(Exception):
def __init__(self, message, statusCode=400): def __init__(self, message, statusCode=400):
super().__init__(message) super().__init__(message)
self.message = message
self.statusCode = statusCode self.statusCode = statusCode

View File

@ -5,19 +5,48 @@ import requests
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
from Mine.Block import Block from Mine.Block import Block
from Mine.Transaction import Transaction
WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/') WIKIDECK_URL = os.getenv('WIKIDECK_URL', 'http://localhost:8080/')
privateKey = rsa.generate_private_key( print("Generating RSA keys...")
privateKeyA = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
privateKeyB = rsa.generate_private_key(
public_exponent=65537, public_exponent=65537,
key_size=4096 key_size=4096
) )
newBlock = Block( print("Getting block to mine from server...")
newBlockA = Block(
data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json() data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json()
) )
newBlock.mine(privateKey) print("Received block for user A to mine.")
print(newBlock) print("Mining block...")
newBlockA.mine(privateKeyA)
r = requests.post(f"{WIKIDECK_URL}/mine/blocks", json=newBlockA.as_dict())
print(json.dumps(r.json(), indent=4))
input("Press enter to continue...")
r = requests.post(f"{WIKIDECK_URL}/mine/blocks", data=newBlock.as_dict()) print("Sending card A to user B...")
print(r) 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))