from dataclasses import dataclass from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.exceptions import InvalidSignature import hashlib from multiprocessing import Lock import time @dataclass class Transaction: id: int sender: bytes receiver: bytes amount: int transaction_fee: int signature: bytes def from_bytes(transaction_raw): assert len(transaction_raw) == 148 return Transaction( id = int.from_bytes(transaction_raw[0:4], "big"), sender = transaction_raw[4:36], receiver = transaction_raw[36:68], amount = int.from_bytes(transaction_raw[68:76], "big"), transaction_fee = int.from_bytes(transaction_raw[76:84], "big"), signature = transaction_raw[84:148], ) def is_valid(self): sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender) msg = self.id.to_bytes(4, "big") + \ self.sender + \ self.receiver + \ self.amount.to_bytes(8, "big") + \ self.transaction_fee.to_bytes(8, "big") try: sender_pubkey.verify(self.signature, msg) except InvalidSignature: return False return self.amount >= 1 def is_valid_after_block(self, block): if (self.sender, self.id) in block.used_transaction_ids: return False balance = block.balances.get(self.sender) if balance is None: return False return balance >= self.amount + self.transaction_fee def get_transaction_raw(self): return self.id.to_bytes(4, "big") + \ self.sender + \ self.receiver + \ self.amount.to_bytes(8, "big") + \ self.transaction_fee.to_bytes(8, "big") + \ self.signature def sorting_id(self): return (-self.transaction_fee, self.sender, self.id) def __eq__(self, other): return (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \ (other.id, other.sender, other.receiver, other.amount, other.transaction_fee) @dataclass class Block: nonce: int timestamp: int previous_hash: bytes message: bytes difficulty_sum: int miner_pubkey: bytes transaction: Transaction own_hash: bytes balances: dict # (sender_pubkey, id) tuples used_transaction_ids: set block_number: int valid: bool def from_bytes(block_raw): assert len(block_raw) == 292 transaction_raw = block_raw[0:148] if transaction_raw == 148 * b"\0": transaction = None else: transaction = Transaction.from_bytes(transaction_raw) return Block( transaction = transaction, message = block_raw[148:180], miner_pubkey = block_raw[180:212], previous_hash = block_raw[212:244], timestamp = int.from_bytes(block_raw[244:252], "big"), difficulty_sum = int.from_bytes(block_raw[252:284], "big"), nonce = int.from_bytes(block_raw[284:292], "big"), own_hash = hashlib.sha256(block_raw).digest(), balances = None, used_transaction_ids = None, block_number = None, valid = False, ) def validate(self, blockchain): if self.transaction is not None: if not self.transaction.is_valid(): return False if self.previous_hash != 32 * b"\0": prev_block = blockchain.get_block(self.previous_hash) if prev_block is None: return False if not prev_block.valid: return False if self.timestamp <= prev_block.timestamp: return False if self.timestamp > time.time(): return False if self.transaction is not None and not self.transaction.is_valid_after_block(prev_block): return False else: prev_block = None if self.transaction is not None: return False # check for the correct miner pubkey - which will become public at launch day h = hashlib.sha256(self.miner_pubkey).hexdigest() if h != "88023d392db35f2d3936abd0532003ae0a38b4d35e4d123a0fa28c568c7e3e2f": return False B_1_difficulty_sum, _ = self.get_difficulty_info(1, blockchain) B_10_difficulty_sum, B_10_timestamp = self.get_difficulty_info(10, blockchain) D = B_1_difficulty_sum - B_10_difficulty_sum T = self.timestamp - B_10_timestamp calculated_difficulty = D * 3000 // 9 // T block_difficulty = max(calculated_difficulty, 2**28) if B_1_difficulty_sum + block_difficulty != self.difficulty_sum: return False self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256 if self.valid: self.calculate_balances(prev_block) self.calculate_used_transaction_ids(prev_block) self.calculate_block_number(prev_block) return self.valid def calculate_balances(self, prev_block): if prev_block is None: self.balances = { self.miner_pubkey: 100, } return balances = prev_block.balances.copy() balances.setdefault(self.miner_pubkey, 0) balances[self.miner_pubkey] += 100 t = self.transaction if t is not None: balances[self.miner_pubkey] += t.transaction_fee balances[t.sender] -= (t.amount + t.transaction_fee) balances[t.receiver] += t.amount self.balances = balances def calculate_used_transaction_ids(self, prev_block): if prev_block is None: self.used_transaction_ids = set() return used_transaction_ids = prev_block.used_transaction_ids.copy() t = self.transaction if t is not None: used_transaction_ids.add((t.sender, t.id)) self.used_transaction_ids = used_transaction_ids def calculate_block_number(self, prev_block): if prev_block is None: self.block_number = 0 else: self.block_number = prev_block.block_number + 1 def get_difficulty_info(self, steps, blockchain): if steps == 0: return self.difficulty_sum, self.timestamp if self.previous_hash == 32 * b"\0": difficulty_sum = 2**29 - steps * 2**28 timestamp = self.timestamp - steps * 300 return difficulty_sum, timestamp else: previous_block = blockchain.get_block(self.previous_hash) return previous_block.get_difficulty_info(steps-1, blockchain) def get_block_raw(self): if self.transaction is None: transaction = 148 * b"\0" else: transaction = self.transaction.get_transaction_raw() return transaction + \ self.message + \ self.miner_pubkey + \ self.previous_hash + \ self.timestamp.to_bytes(8, "big") + \ self.difficulty_sum.to_bytes(32, "big") + \ self.nonce.to_bytes(8, "big") class OpenTransactions: def __init__(self, blockchain): self.__blockchain = blockchain self.__open_transactions = [] self.__lock = Lock() self.__recalculate_hashes() def add(self, transaction): assert transaction.is_valid() with self.__lock: # pre-check 1: Check for duplicates for existing_transaction in self.__open_transactions: if transaction == existing_transaction: return # pre-check 2: Check if there is space if not self.__has_space(transaction): return # Add the transaction self.__open_transactions.append(transaction) self.__cleanup() def update(self): with self.__lock: self.__cleanup() def get_hash(self, i): assert i < 1024 with self.__lock: return self.__hashes[i] def get_transaction(self, i): with self.__lock: if i >= len(self.__open_transactions): return None return self.__open_transactions[i] def __has_space(self, transaction): if len(self.__open_transactions) < 1000: return True return transaction.sorting_id() < self.__open_transactions[-1].sorting_id() def __cleanup(self): # sort everything self.__open_transactions.sort(key = Transaction.sorting_id) # drop out invalid ones # - reused ids # - paying more money than available latest_block = self.__blockchain.get_latest_block() if latest_block is None: self.__open_transactions = [] self.__recalculate_hashes() return used_transaction_ids = latest_block.used_transaction_ids.copy() balances = latest_block.balances.copy() def is_valid(transaction): sender_tuple = (transaction.sender, transaction.id) if sender_tuple in used_transaction_ids: return False balance = balances.get(transaction.sender) or 0 if transaction.amount + transaction.transaction_fee > balance: return False used_transaction_ids.add(sender_tuple) balances[transaction.sender] = balance - transaction.amount - transaction.transaction_fee return True self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)] # limit to 1024 self.__open_transactions = self.__open_transactions[0:1024] self.__recalculate_hashes() def __recalculate_hashes(self): self.__hashes = [] current_hash = 32 * b"\0" for i in range(1024): if i >= len(self.__open_transactions): transaction_data = 44 * b"\0" else: transaction = self.__open_transactions[i] transaction_data = transaction.transaction_fee.to_bytes(8, "big") + \ transaction.sender + \ transaction.id.to_bytes(4, "big") current_hash = hashlib.sha256(current_hash + transaction_data).digest() self.__hashes.append(current_hash) class Blockchain: def __init__(self): # maps block hashes to block instances self.__block_map = {} self.__latest_block_hash = None self.__lock = Lock() self.open_transactions = OpenTransactions(self) self.__load_blocks_from_disk() def __load_blocks_from_disk(self): last_valid = None try: with open("blockchain", "rb") as f: while True: block = f.read(292) if len(block) < 292: break block_obj = self.add_block(block) if not block_obj.validate(self): break last_valid = block_obj except FileNotFoundError: pass if last_valid is not None: self.set_latest_block(last_valid.own_hash, persist = False) def set_latest_block(self, block_hash, persist = True): new_block = self.get_block(block_hash) assert new_block is not None assert new_block.valid while True: with self.__lock: latest_block_hash = self.__latest_block_hash if latest_block_hash is not None: latest_block = self.get_block(latest_block_hash) current_difficulty_sum = latest_block.get_difficulty_info(1, self)[0] new_difficulty_sum = new_block.get_difficulty_info(1, self)[0] if new_difficulty_sum <= current_difficulty_sum: return False else: latest_block = None with self.__lock: if self.__latest_block_hash != latest_block_hash: continue self.__latest_block_hash = block_hash if persist: self.__persist_block_update(latest_block, new_block) self.open_transactions.update() return True def __persist_block_update(self, old_block, new_block): if old_block is not None: while old_block.block_number > new_block.block_number: old_block = self.__block_map[old_block.previous_hash] block_list = [] while new_block is not old_block: block_list.append(new_block) if new_block.previous_hash == 32 * b"\0": break new_block = self.__block_map[new_block.previous_hash] if old_block is not None and old_block.block_number > new_block.block_number: old_block = self.__block_map[old_block.previous_hash] start_block_number = block_list[-1].block_number start_addr = start_block_number * 292 open_mode = "wb" if start_addr == 0 else "r+b" with open("blockchain", open_mode) as f: f.seek(start_addr) for block in reversed(block_list): f.write(block.get_block_raw()) def add_block(self, block_raw): with self.__lock: block = Block.from_bytes(block_raw) if block.own_hash not in self.__block_map: self.__block_map[block.own_hash] = block return self.__block_map[block.own_hash] def get_block(self, hash): with self.__lock: return self.__block_map.get(hash) def get_second_last_difficulty_sum(self): with self.__lock: latest_block_hash = self.__latest_block_hash if latest_block_hash is None: return 0 block = self.get_block(latest_block_hash) return block.get_difficulty_info(1, self)[0] def get_latest_block(self): with self.__lock: if self.__latest_block_hash is None: return None return self.__block_map[self.__latest_block_hash]