wikideck/wikideck/Mine/Mine.py

243 lines
8.5 KiB
Python

import datetime
import flask
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
from Mine.Card import Card
from Mine.Database import Database
from Mine.Transaction import Transaction
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))
mine = flask.Blueprint("mine", __name__)
db = Database()
privateKey = rsa.generate_private_key(
public_exponent=65537,
key_size=4096
)
def save_block(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)
# 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():
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():
try:
blocks = db.get_blocks()
return flask.jsonify([block.as_dict() for block in blocks]) if blocks else flask.jsonify({})
except Exception as e:
return flask.jsonify(
{'Error': str(e)}
), e.statusCode if hasattr(
e, 'statusCode'
) else 500
###
# 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.
###
@mine.get('/blocks')
def blocks_get():
# TODO: block queries
blockId = flask.request.args.get('blockId', None)
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 = db.get_pending_transactions()
).as_dict()
)
###
# Submit a block to the chain.
# This method calls the Block().validate() method to validate the block schema.
# This method performs additional validations against the rest of the chain.
###
@mine.post('/blocks')
def blocks_post():
try:
newBlock = Block(data=flask.request.get_json())
newBlock.validate()
previousBlock = db.get_last_block()
if newBlock.previousHash != previousBlock.blockHash.hexdigest():
raise Block.Invalid(
f"Previous hash should be {previousBlock.blockHash.hexdigest()}."
)
if newBlock.timestamp <= previousBlock.timestamp:
raise Block.Invalid(
f"Timestamp {newBlock.timestamp} is before {previousBlock.timestamp}."
)
if newBlock.height != previousBlock.height + 1:
raise Block.Invalid(
f"Height should be {previousBlock.height + 1} not {newBlock.height}."
)
if newBlock.difficulty < DIFFICULTY_REQUIREMENT:
raise Block.Invalid(
f"Difficulty should be {DIFFICULTY_REQUIREMENT} not {newBlock.difficulty}."
)
if len(newBlock.transactions) == 0:
raise Block.Invalid(
"Block contains no transactions."
)
for transaction in newBlock.transactions:
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 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
###
@mine.get('/cards')
def cards_get():
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 [])
else:
return flask.jsonify({'Error': "No public key."}), 400
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.
# This method performs a number of validations on the submitted transaction and returns
# a status code result.
###
@mine.post('/transactions')
def transactions_post():
try:
newTransaction = Transaction(data=flask.request.get_json())
newTransaction.validate()
if not db.get_card_by_id(newTransaction.cardId):
raise Transaction.Invalid(
f"Card {newTransaction.cardId} does not exist.",
404
)
if newTransaction.sender != db.get_card_owner(newTransaction.cardId):
raise Transaction.Unauthorized(
f"{newTransaction.sender} does not own {newTransaction.cardId}."
)
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 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.
###
@mine.get('/transactions')
def transactions_get():
transactionId = request.args.get('transactionId', None)
return get_transaction_by_id(transactionId) if transactionId else json.dumps(
db.get_pending_transactions()
)
@mine.post('/peers')
def peers_post():
try:
peer = Peer(data=request.get_json())
peer.validate()
# TODO: add peers to database
return 200
# TODO: error handling
except Exception as e:
print('crap!')
@mine.get('/peers')
def peers_get():
# TODO: query peers
return 200