# Adatbiztonság és kriptográfia (2018)
# Csutak Balázs (ABDTW1)
# csutak.balazs@hallgato.ppke.hu
# PPKE-ITK
# 2. gyakorlat


#!/usr/bin/env python
# -*- coding: utf-8 -*-


from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
import json
from base64 import b64encode, b64decode
from time import time
import pickle
import pprint
import os

class Blockchain:
    def __init__(self):
        self.current_transactions = [] #egy list amiben tároljuk az aktuális saját kimenő tranzakciókat, majd vagy kimentjük ha nem akarjuk kibányászni vagy bányászás elején belerakjuk a többiek tranzakcióit
        self.chain = [] #egy list amiben a blockchaint tároljuk
        f = open('rsa_key.pem', 'rb');
        key = RSA.importKey(f.read())
        self.private_key = key #kiolvassuk a saját kulcsunkat a fájlból amit korábban létrehoztunk
        self.public_key= key.publickey() #itt is
        self.my_inputs =  [
             {'signature': 'valami_aláírás_van_itt_1', 'amount': 10},
             {'signature': 'valami_aláírás_van_itt_2', 'amount': 2},
             {'signature': 'valami_aláírás_van_itt_3', 'amount': 5},
             {'signature': 'valami_aláírás_van_itt_4', 'amount': 4},
         ]

    def sign_data(self, data):
        signer = PKCS1_v1_5.new(self.private_key)
        hash = self.hash(data)
        sign = signer.sign(hash)
        return b64encode(sign).decode()

    def verify(self, data, signature, pub_key):
        signer = PKCS1_v1_5.new(pub_key)
        hash = self.hash(data)
        return signer.verify(hash, b64decode(signature.encode()))

    @staticmethod
    def hash(data):
        hash = SHA256.new()
        encoded_data = json.dumps(data, sort_keys=True).encode()
        hash.update(encoded_data)
        return hash

    def my_balance(self): #egy függvény ami visszatér az aktuális egyenlegemmel
        balance = 0;
        for input in self.my_inputs:
            balance+=input['amount'];
        return balance

    def new_transaction(self, recipient_public_key, amount): #egy új tranzakció létrehozása. Csak egész (int) összegeket lehet utalni
        f = open(recipient_public_key, 'rb')
        recipient = RSA.importKey(f.read())

        amount = int(amount)
        # Keressünk először a tranzakciókból annyit, amennyi elég lesz a kívánt összeg utalásához.
        sum = 0
        inputs = []
        if (self.my_balance() >= amount):
            while (sum < amount):
                input = self.my_inputs.pop();
                sum += input['amount'];
                inputs.append(input);
                # kiszedünk a my_inputs listából egyet (úgy hogy töröljük is)
                # hozzáadjuk az inputs listához
                # a sum értékét növeljük a hozzáadott input értékével

            transaction = {
                'inputs': inputs,
                'sender': self.public_key.exportKey('PEM').decode(),
                'recipient': recipient.exportKey('PEM').decode(),
                'amount': amount,
                'sendback': sum - amount,
                'time': time() # ez csakúgy van benne, nem igazán használjuk
            }
            
            
            signature = self.sign_data(transaction);
            # írjuk alá a tranzakciót
            # az aláírással együtt adjuk hozzá a current_transaction listához, amiben az aktuális,
            # még nem könyvelt (nem blokkban lévő) tranzakciókat tároljuk
            self.current_transactions.append({'transaction': transaction, 'signature': signature})
            
            # ez nem kell, csak tesztelni
            return {'transaction': transaction, 'signature': signature}
        else:
            print("Nincs elég pénzed.")

        return

    def validate_transaction(self, transaction, blockchain=None): #egy tranzakció igazolására szolgáló függvény (lehet egy nem default blockchain tekintetében is vizsgálni)
        transaction_content = transaction['transaction']
        signature = transaction['signature']
        sender = RSA.importKey(transaction_content['sender'].encode())

        # itt sok if feltétel van, bármelyiken elbukik a tranzakció, akkor False értékkel térünk vissza

        # ha az aláírás nem stimmel
        if self.verify(transaction_content, signature, sender) == False:
            return False;
            
        # ha nem egész összeget utalunk, vagy nem egész összeg a visszajáró

        if transaction_content['amount'] != int(transaction_content['amount']) or transaction_content['sendback'] != int(transaction_content['sendback']):
            return False;
        
        # ha akár az utalt összeg, akár a visszajáró negatív

        if transaction_content['amount'] <= 0 or transaction_content['sendback'] < 0:
            return False;
        # ha azonos a kedvezményezett, mint a küldő fél

        if transaction_content['sender'] == transaction_content['recipient']:
            return False;
        
        # a get_transaction függvény visszaadja a sender user felhasználható tranzakcióit, szóval egy olyan listát, amiben {'signature', 'amount'} dictek vannak
        # ezt a listát nevezzük valid transactions-nek, hiszen ha ebből rak az inputba, akkor azt jogosan használja fel.
        valid_transactions = self.get_transactions(sender, blockchain)

        #ezt a listát kell összehasonlítani a tranzakció inputjában adott listával és ha van benne olyan ami a validban nincs, akkor return False

        sum = 0;
        for input in transaction_content['inputs']:
            if ((input in valid_transactions) == False):
                return False;
            sum += input['amount'];
        # Ez a csúnyaság azért van itt, ha egy korábbi tranzakciót többször is bele akart rakni a bemenetek közé, akkor kiszűrje. Biztos lehet elegánsabban, akár az előzővel egybevéve
        if len([d['signature'] for d in transaction_content['inputs']]) != len(set([d['signature'] for d in transaction_content['inputs']])):
            return False

        # Vizsgáljuk meg, hogy a megadott és az ezen a ponton már érvényes inputok elég fedezetet biztosítanak-e az utalás értékére és a visszajáróra

        if sum < transaction_content['amount'] + transaction_content['sendback']:
            return False;
        
        return True

    @staticmethod
    def PoW(hash):
        return hash[:4] == "0000"

    def mine(self):

        transaction = {
        # berakunk egy input nélküli tranzakciót a tranzakciók elejére és ebben kapjuk meg a bányászjutalmat
            'inputs': [],
            'sender': self.public_key.exportKey('PEM').decode(),
            'recipient': self.public_key.exportKey('PEM').decode(),
            'amount': 10,  # a bányászásért járó jutalom
            'sendback': 0,
            'time': time()
        }
        signature = self.sign_data(transaction)
        transactions_to_block = [{'transaction': transaction, 'signature': signature}] # ebben a listában tároljuk a blokkba könyvelt tranzakciókat, a jutalom az első elem

        # végigmegyünk a current_transactions listán és megvizsgáljuk egyenként a tranzakciókat, hogy belerakjuk-e a blokkba vagy nem
        # az előbb elkészített validate_transaction függvénnyel. Ha érvényes tranzakció, belerakjuk a gyűjteménybe
        
        for tr in self.current_transactions:
            if self.validate_transaction(tr):
                transactions_to_block.append(tr)
                

        #összeállítjuk a blokkot
        # a blokkok egymásba fűzése miatt az előző blokk hash-e bekerül a következőbe. Az első blokknak nincs megelőzője.
        if len(self.chain) == 0:
            previous_hash = 1
        else:
            previous_hash = self.hash(self.chain[-1]).hexdigest()

        block = {
            'index': len(self.chain),
            'transactions':  transactions_to_block,
            'nonce': 0, # a nonce mint futóindex kiinduló értéke,
            'previous_hash': previous_hash
        }
        while True:
            hash = self.hash(block).hexdigest();
            if self.PoW(hash) == False:
                block['nonce'] += 1;
            else:
                break;
            # a proof of work: amíg a block hashének hexadecimális ábrázolás nem 4db nullával kezdődik, addig növeljük folyamatosan a nonce értékét

        print(block['nonce']);
        # kiürítjuk a tranzakciólistát, hiszen ezek már könyveltek lettek
        self.current_transactions = []
        # kigyűjtjük a ránk vonatkozó utalásokat és frissítjük vele a my_inputs listát
        self.my_inputs.extend(self.get_transactions(self.public_key, [block]))
        #hozzáadjuk a blokkot a chain-hez
        self.chain.append(block)
        # kimentjük az aktuális blockchaint
        self.save_obj(self.chain, "obj", "blockchain.pkl")
        return block


    def validate_first_transaction(self, transaction):
        if len(transaction['transaction']['inputs']) != 0 or transaction['transaction']['amount'] != 10 or transaction['transaction']['sendback'] != 0:
            return False
        return True

    def get_transactions(self, sender, blockchain = None):
        return [
             {'signature': 'valami_aláírás_van_itt_1', 'amount': 10},
             {'signature': 'valami_aláírás_van_itt_2', 'amount': 2},
             {'signature': 'valami_aláírás_van_itt_3', 'amount': 5},
             {'signature': 'valami_aláírás_van_itt_4', 'amount': 4}
         ]

    def export_transactions(self):
        self.save_obj(self.current_transactions, "obj", "transactions.pkl")

    def import_transactions(self):
        for file in os.listdir("transactions"):
            if file.endswith(".pkl"):
                self.current_transactions.extend(self.load_obj("transactions", file))

    @staticmethod
    def save_obj(obj, dir, name):
        with open(os.path.join(dir, name), 'wb') as f:
            pickle.dump(obj, f)

    @staticmethod
    def load_obj(dir, name):
        with open(os.path.join(dir, name), 'rb') as f:
            return pickle.load(f)

            
    
            
            
bc = Blockchain()

# print(bc.my_balance());
# tr = bc.new_transaction("pub2.pem", 8);
# print(bc.validate_transaction(tr));
# bc.mine();
# pprint.pprint(bc.chain, width=1)

bc.new_transaction('pub2.pem',3)
print(bc.current_transactions)
bc.mine()
pprint.pprint(bc.chain, width=1)

