forked from WikiDeck/wikideck
243 lines
8.5 KiB
Python
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
|