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

View File

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

View File

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

View File

@ -1,44 +1,61 @@
import base64
import datetime
import mariadb
import os
import time
import uuid
from cryptography.hazmat.primitives import serialization
from Mine.Block import Block
from Mine.Card import Card
from Mine.Transaction import Transaction
class Database():
SQL_CREATE_DATABASE = [
SQL_CREATE_TABLES = [
"""
CREATE TABLE IF NOT EXISTS mine.blocks(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) UNIQUE NOT NULL,
blockId UUID UNIQUE NOT NULL,
blockHash VARCHAR(64) UNIQUE NOT NULL,
previousHash VARCHAR(64) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL,
timestamp VARCHAR(32) NOT NULL,
height INT UNIQUE NOT NULL,
difficulty INT NOT NULL,
nonce INT NOT NULL
);
""",
"""
CREATE TABLE IF NOT EXISTS mine.cards(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) UNIQUE NOT NULL,
cardId VARCHAR(37) UNIQUE NOT NULL,
blockId UUID UNIQUE NOT NULL,
cardId UUID UNIQUE NOT NULL,
pageId INT NOT NULL,
FOREIGN KEY (blockId) REFERENCES blocks(blockId)
);
""",
"""
CREATE TABLE IF NOT EXISTS mine.pending_transactions(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
transactionId UUID UNIQUE NOT NULL,
cardId UUID NOT NULL,
timestamp VARCHAR(32) NOT NULL,
sender TEXT NOT NULL,
receiver TEXT NOT NULL,
signature TEXT NOT NULL,
FOREIGN KEY (cardId) REFERENCES cards(cardId)
);
""",
"""
CREATE TABLE IF NOT EXISTS mine.transactions(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
blockId VARCHAR(37) NOT NULL,
cardId VARCHAR(37) UNIQUE NOT NULL,
transactionId VARCHAR(37) UNIQUE NOT NULL,
timestamp DATETIME NOT NULL,
sender VARCHAR(128) NOT NULL,
receiver VARCHAR(128) NOT NULL,
signature VARCHAR(128) NOT NULL,
isPending BOOLEAN NOT NULL DEFAULT true,
isAbandoned BOOLEAN NOT NULL DEFAULT false,
blockId UUID NOT NULL,
transactionId UUID UNIQUE NOT NULL,
cardId UUID NOT NULL,
timestamp VARCHAR(32) NOT NULL,
sender TEXT NOT NULL,
receiver TEXT NOT NULL,
signature TEXT NOT NULL,
FOREIGN KEY (blockId) REFERENCES blocks(blockId),
FOREIGN KEY (cardId) REFERENCES cards(cardId)
);
@ -46,7 +63,7 @@ class Database():
"""
CREATE TABLE IF NOT EXISTS mine.peers(
sqlId INT PRIMARY KEY AUTO_INCREMENT,
peerId VARCHAR(37) UNIQUE NOT NULL,
peerId UUID UNIQUE NOT NULL,
baseUrl VARCHAR(128) UNIQUE NOT NULL,
isUp BOOLEAN NOT NULL,
downCount INT DEFAULT 0,
@ -58,61 +75,89 @@ class Database():
SQL_GET_BLOCK_BY_ID = """
SELECT *
FROM blocks
WHERE blockId = '{}';
WHERE blockId = ?;
"""
SQL_GET_LAST_BLOCK = """
SELECT *
FROM blocks
ORDER BY timestamp DESC
LIMIT 1;
"""
SQL_GET_CARD = """
SELECT *
SQL_GET_CARD_BY_ID = """
SELECT cardId, pageId
FROM cards
WHERE cardId = '{}';
WHERE cardId = ?;
"""
SQL_GET_CARD_BY_BLOCK_ID = """
SELECT cardId, pageId
FROM cards
WHERE blockId = ?;
"""
SQL_GET_CARD_OWNER = """
SELECT receiver
FROM transactions
WHERE cardId = '{}'
AND isPending = False
WHERE cardId = ?
ORDER BY timestamp DESC
LIMIT 1;
"""
SQL_GET_PENDING_TRANSACTIONS = """
SELECT *
FROM transactions
WHERE isPending = True
ORDER BY timestamp ASC
LIMIT {};
"""
SQL_GET_TRANSACTION_BY_ID = """
SELECT *
FROM transactions
WHERE transactionId == '{}'
"""
SQL_INSERT_BLOCK = """
INSERT INTO blocks (blockId, blockHash, previousHash, timestamp, height, nonce)
VALUES ('{}', '{}', '{}', '{}', {}, {});
INSERT INTO mine.blocks (
blockId, blockHash, previousHash, timestamp, height, difficulty, nonce
)
VALUES (?, ?, ?, ?, ?, ?, ?);
"""
SQL_GET_LAST_BLOCK = """
SELECT blockId, previousHash, timestamp, height, difficulty, nonce
FROM mine.blocks
ORDER BY timestamp DESC
LIMIT 1;
"""
SQL_INSERT_CARD = """
INSERT INTO mine.cards (blockId, cardId, pageId) VALUES (?, ?, ?);
"""
SQL_INSERT_PENDING_TRANSACTION = """
INSERT INTO mine.pending_transactions (
transactionId, timestamp, cardId, sender, receiver, signature
)
VALUES (?, ?, ?, ?, ?, ?);
"""
SQL_GET_PENDING_TRANSACTIONS = """
SELECT transactionId, timestamp, cardId, sender, receiver, signature
FROM mine.pending_transactions
ORDER BY timestamp ASC
LIMIT ?;
"""
SQL_DELETE_PENDING_TRANSACTION = """
DELETE FROM mine.pending_transactions WHERE transactionId = ?;
"""
SQL_INSERT_TRANSACTION = """
INSERT INTO transactions (
transactionId,
timestamp,
sender,
receiver,
signature,
blockId,
cardId
INSERT INTO mine.transactions (
blockId, transactionId, timestamp, cardId, sender, receiver, signature
)
VALUES ({}, {}, {}, {}, {});
VALUES (?, ?, ?, ?, ?, ?, ?);
"""
SQL_GET_PENDING_TRANSACTION_BY_ID = """
SELECT transactionId, timestamp, cardId, sender, receiver, signature
FROM pending_transactions
WHERE transactionId = ?;
"""
SQL_GET_TRANSACTION_BY_ID = """
SELECT transactionId, timestamp, cardId, sender, receiver, signature
FROM transactions
WHERE transactionId = ?;
"""
SQL_GET_TRANSACTIONS_BY_BLOCK_ID = """
SELECT transactionId, timestamp, cardId, sender, receiver, signature
FROM mine.transactions
WHERE blockId = ?
ORDER BY timestamp DESC;
"""
def __init__(self):
@ -124,67 +169,229 @@ class Database():
port = os.getenv('MINE_DB_PORT', 3306),
user = os.getenv('MINE_DB_USER', None),
password = os.getenv('MINE_DB_PASSWORD', None),
database = 'mine'
database = 'mine',
autocommit = True
)
for each in self.SQL_CREATE_DATABASE:
cursor = self.conn.cursor().execute(each)
for each in self.SQL_CREATE_TABLES:
self.conn.cursor().execute(each)
except mariadb.Error as e:
time.sleep(delay := delay**2)
continue
break
def get_block_by_id(self, blockId):
return Block(
data=self.conn.cursor().execute(
self.SQL_GET_BLOCK_BY_ID.format(blockId)
cur = self.conn.cursor()
cur.execute(self.SQL_GET_BLOCK_BY_ID, [blockId])
block = cur.fetchone()
if block:
blockCard = self.get_card_by_block_id(lastBlock[1])
blockTransactions = self.get_transactions_by_block_id(lastBlock[1])
return Block(
blockId = uuid.UUID(block[1]),
previousHash = block[3],
timestamp = block[4],
height = block[5],
nonce = block[6],
difficulty = block[7],
card = blockCard,
transactions = blockTransactions
)
else:
return False
def get_card_by_id(self, cardId):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_CARD_BY_ID, [str(cardId)])
card = cur.fetchone()
return Card(
cardId = uuid.UUID(card[0]),
pageId = card[1]
)
def get_card_by_block_id(self, blockId):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_CARD_BY_BLOCK_ID, [str(blockId)])
card = cur.fetchone()
return Card(
cardId = uuid.UUID(card[0]),
pageId = card[1]
)
def get_last_block(self):
data=self.conn.cursor().execute(self.SQL_GET_LAST_BLOCK)
return Block(data=data) if data else None
def get_card(self, cardId):
return Card(
data=self.conn.cursor().execute(self.SQL_GET_CARD.format(cardId))
)
cur = self.conn.cursor()
cur.execute(self.SQL_GET_LAST_BLOCK)
lastBlock = cur.fetchone()
if lastBlock:
lastBlockCard = self.get_card_by_block_id(lastBlock[0])
lastBlockTransactions = self.get_transactions_by_block_id(lastBlock[0])
return Block(
blockId = uuid.UUID(lastBlock[0]),
previousHash = lastBlock[1],
timestamp = datetime.datetime.strptime(
lastBlock[2],
"%Y-%m-%d %H:%M:%S.%f%z"
),
height = lastBlock[3],
difficulty = lastBlock[4],
nonce = lastBlock[5],
card = lastBlockCard,
transactions = lastBlockTransactions
)
def get_card_owner(self, cardId):
return self.conn.cursor().execute(
self.SQL_GET_CARD_OWNER.format(cardId)
)
cur = self.conn.cursor()
cur.execute(self.SQL_GET_CARD_OWNER, [str(cardId)])
owner = cur.fetchone()
return serialization.load_pem_public_key(
owner[0].encode('utf-8')
) if owner else None
def get_pending_transactions(self, limit=32768):
pendingTransactions = self.conn.cursor().execute(
self.SQL_GET_PENDING_TRANSACTIONS.format(limit)
cur = self.conn.cursor()
cur.execute(
self.SQL_GET_PENDING_TRANSACTIONS,
[limit]
)
pendingTransactions = cur.fetchall()
return [
Transaction(
data=each
) for each in pendingTransactions
transactionId = uuid.UUID(pendingTransaction[0]),
timestamp = datetime.datetime.strptime(
pendingTransaction[1],
"%Y-%m-%d %H:%M:%S.%f%z"
),
cardId = uuid.UUID(pendingTransaction[2]),
# TODO: load rsa keys
sender = serialization.load_pem_public_key(
pendingTransaction[3].encode('utf-8')
),
receiver = serialization.load_pem_public_key(
pendingTransaction[4].encode('utf-8')
),
signature = bytes.fromhex(pendingTransaction[5])
) for pendingTransaction in pendingTransactions
] if pendingTransactions else []
def get_transaction(self, transactionId):
return Transaction(
data=self.conn.cursor().execute(self.SQL_GET_TRANSACTION.format(transactionId))
)
def get_pending_transaction_by_id(self, transactionId):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_PENDING_TRANSACTION_BY_ID, [str(transactionId)])
transaction = cur.fetchone()
if transaction:
return Transaction(
transactionId = transaction[0],
timestamp = datetime.datetime.strptime(
transaction[1],
"%Y-%m-%d %H:%M:%S.%f%z"
),
cardId = uuid.UUID(transaction[2]),
sender = serialization.load_pem_public_key(
transaction[3].encode('utf-8')
),
receiver = serialization.load_pem_public_key(
transaction[4].encode('utf-8')
),
signature = bytes.fromhex(transaction[5])
)
def get_transaction_by_id(self, transactionId):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_TRANSACTION_BY_ID, [str(transactionId)])
transaction = cur.fetchone()
if transaction:
return Transaction(
transactionId = transaction[0],
timestamp = datetime.datetime.strptime(
transaction[1],
"%Y-%m-%d %H:%M:%S.%f%z"
),
cardId = uuid.UUID(transaction[2]),
sender = serialization.load_pem_public_key(
transaction[3].encode('utf-8')
),
receiver = serialization.load_pem_public_key(
transaction[4].encode('utf-8')
),
signature = bytes.fromhex(transaction[5])
)
def get_transactions_by_block_id(self, blockId):
cur = self.conn.cursor()
cur.execute(self.SQL_GET_TRANSACTIONS_BY_BLOCK_ID, [blockId])
transactions = cur.fetchall()
return [
Transaction(
transactionId = transaction[0],
timestamp = datetime.datetime.strptime(
transaction[1],
"%Y-%m-%d %H:%M:%S.%f%z"
),
cardId = transaction[2],
sender = serialization.load_pem_public_key(
transaction[3].encode('utf-8')
),
receiver = serialization.load_pem_public_key(
transaction[4].encode('utf-8')
),
signature = bytes.fromhex(transaction[5])
) for transaction in transactions
]
def insert_block(self, block):
self.conn.cursor().execute(self.SQL_INSERT_BLOCK.format(
cur = self.conn.cursor()
cur.execute(self.SQL_INSERT_BLOCK, (
str(block.blockId),
block.blockHash.hexdigest(),
block.previousHash,
str(block.timestamp),
block.height,
block.difficulty,
block.nonce
))
def insert_transaction(self, transaction):
return self.conn.cursor().execute(self.SQL_INSERT_TRANSACTION.format(
def insert_card(self, blockId, card):
cur = self.conn.cursor()
cur.execute(self.SQL_INSERT_CARD, (
str(blockId),
str(card.cardId),
card.pageId
))
def insert_pending_transaction(self, transaction):
cur = self.conn.cursor()
cur.execute(self.SQL_INSERT_PENDING_TRANSACTION, (
str(transaction.transactionId),
str(transaction.timestamp),
transaction.sender,
transaction.receiver,
transaction.signature,
str(transaction.cardId)
str(transaction.cardId),
transaction.sender.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
transaction.receiver.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
transaction.signature.hex()
))
def delete_pending_transaction(self, transactionId):
cur = self.conn.cursor()
cur.execute(self.SQL_DELETE_PENDING_TRANSACTION, [str(transactionId)])
def insert_transaction(self, blockId, transaction):
cur = self.conn.cursor()
cur.execute(self.SQL_INSERT_TRANSACTION, (
str(blockId),
str(transaction.transactionId),
str(transaction.timestamp),
str(transaction.cardId),
transaction.sender.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
transaction.receiver.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8'),
transaction.signature.hex()
))

View File

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

View File

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

View File

@ -5,19 +5,48 @@ 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/')
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,
key_size=4096
)
newBlock = Block(
print("Getting block to mine from server...")
newBlockA = Block(
data = requests.get(f"{WIKIDECK_URL}/mine/blocks").json()
)
newBlock.mine(privateKey)
print(newBlock)
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...")
r = requests.post(f"{WIKIDECK_URL}/mine/blocks", data=newBlock.as_dict())
print(r)
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))