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 n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143 class Transaction: pass class NoTransaction(Transaction): def is_valid(self): return True def is_valid_after_block(self, block): return True def get_transaction_raw(self): return 149 * b"\0" def is_empty(self): return True @dataclass class PaymentTransaction(Transaction): id: int sender: bytes receiver: bytes amount: int transaction_fee: int signature: bytes def from_bytes(transaction_raw): return PaymentTransaction( id = int.from_bytes(transaction_raw[1:5], "big"), sender = transaction_raw[5:37], receiver = transaction_raw[37:69], amount = int.from_bytes(transaction_raw[69:77], "big"), transaction_fee = int.from_bytes(transaction_raw[77:85], "big"), signature = transaction_raw[85:149], ) def is_valid(self): sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender) msg = b"\x01" + \ 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 b"\x01" + \ 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 open_transactions_hash_data(self): return b"\0" + \ self.transaction_fee.to_bytes(8, "big") + \ self.amount.to_bytes(8, "big") + \ self.sender + \ self.id.to_bytes(4, "big") def sorting_id(self): return (1, -self.transaction_fee, -self.amount, self.sender, self.id) def is_empty(self): return False def __eq__(self, other): return isinstance(other, PaymentTransaction) \ and (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \ (other.id, other.sender, other.receiver, other.amount, other.transaction_fee) @dataclass class GamblingTransaction(Transaction): id: int player: bytes amount: int transaction_fee: int signature: bytes def from_bytes(transaction_raw): if transaction_raw[117:149] != 32 * b"\0": return InvalidTransaction() return GamblingTransaction( id = int.from_bytes(transaction_raw[1:5], "big"), player = transaction_raw[5:37], amount = int.from_bytes(transaction_raw[37:45], "big"), transaction_fee = int.from_bytes(transaction_raw[45:53], "big"), signature = transaction_raw[53:117], ) def is_valid(self): player_pubkey = Ed25519PublicKey.from_public_bytes(self.player) msg = b"\x02" + \ self.id.to_bytes(4, "big") + \ self.player + \ self.amount.to_bytes(8, "big") + \ self.transaction_fee.to_bytes(8, "big") try: player_pubkey.verify(self.signature, msg) except InvalidSignature: return False return self.amount >= 1 def is_valid_after_block(self, block): if (self.player, self.id) in block.used_transaction_ids: return False balance = block.balances.get(self.player) if balance is None: return False return balance >= self.amount + self.transaction_fee def get_transaction_raw(self): return b"\x02" + \ self.id.to_bytes(4, "big") + \ self.player + \ self.amount.to_bytes(8, "big") + \ self.transaction_fee.to_bytes(8, "big") + \ self.signature + \ 32 * b"\0" def open_transactions_hash_data(self): return b"\0" + \ self.transaction_fee.to_bytes(8, "big") + \ self.amount.to_bytes(8, "big") + \ self.player + \ self.id.to_bytes(4, "big") def sorting_id(self): return (1, -self.transaction_fee, -self.amount, self.player, self.id) def is_empty(self): return False def __eq__(self, other): return isinstance(other, GamblingTransaction) \ and (self.id, self.player, self.amount, self.transaction_fee) == \ (other.id, other.player, other.amount, other.transaction_fee) def is_valid_group_element(x): return x > 0 and x <= n//2 def normalize(x): if x <= n//2: return x return n - x @dataclass class RevealTransaction: revealer_pubkey: bytes associated_proof_hash: bytes reveal_value: bytes intermediates: list def from_bytes(transaction_raw, associated_proof_data): if transaction_raw[65:149] != 84 * b"\0": return InvalidTransaction() return RevealTransaction( revealer_pubkey = transaction_raw[1:33], associated_proof_hash = transaction_raw[33:65], reveal_value = associated_proof_data[0:256], intermediates = [associated_proof_data[(i+1)*256:(i+2)*256] for i in range(20)], ) def is_valid(self): to_hash = self.get_associated_data() return hashlib.sha256(to_hash).digest() == self.associated_proof_hash def is_valid_after_block(self, block): if len(block.pending_commitment_blocks) == 0: return False claim_R = int.from_bytes(self.reveal_value, "big") if not is_valid_group_element(claim_R): return False intermediate_numbers = [int.from_bytes(i, "big") for i in self.intermediates] for intermediate in intermediate_numbers: if not is_valid_group_element(intermediate): return False claim_H = int.from_bytes(block.pending_commitment_blocks[0][0], "big") for c in range(20): i = 30 - c claim_I = intermediate_numbers[c] to_hash = self.revealer_pubkey + \ claim_H.to_bytes(256, "big") + \ claim_I.to_bytes(256, "big") + \ claim_R.to_bytes(256, "big") + \ i.to_bytes(1, "big") e = int.from_bytes(hashlib.sha256(to_hash).digest(), "big") new_H = normalize((claim_H * pow(claim_I, e, n)) % n) new_R = normalize((claim_I * pow(claim_R, e, n)) % n) claim_H = new_H claim_R = new_R return normalize(pow(claim_H, 2**1024, n)) == claim_R def get_transaction_raw(self): return b"\x03" + \ self.revealer_pubkey + \ self.associated_proof_hash + \ 84 * b"\0" def open_transactions_hash_data(self): return b"\x01" + 52 * b"\0" def get_associated_data(self): return b"".join([self.reveal_value] + self.intermediates) def sorting_id(self): return (0,) def is_empty(self): return False def __eq__(self, other): return isinstance(other, RevealTransaction) \ and (self.revealer_pubkey, self.associated_proof_hash, self.reveal_value, self.intermediates) == \ (other.revealer_pubkey, other.associated_proof_hash, other.reveal_value, other.intermediates) class InvalidTransaction: def is_valid(self): return False def associated_data_required(transaction): return transaction[0] == 3 and transaction[65:149] == 84 * b"\0" def transaction_from_bytes(transaction_raw, associated_data = None): assert len(transaction_raw) == 149 if transaction_raw == 149 * b"\0": return NoTransaction() elif transaction_raw[0] == 1: return PaymentTransaction.from_bytes(transaction_raw) elif transaction_raw[0] == 2: return GamblingTransaction.from_bytes(transaction_raw) elif transaction_raw[0] == 3: return RevealTransaction.from_bytes(transaction_raw, associated_data) else: return InvalidTransaction() @dataclass class Block: nonce: int timestamp: int previous_hash: bytes message: bytes difficulty_sum: int miner_pubkey: bytes transaction: Transaction own_hash: bytes pending_commitment_blocks: list pending_gambling_transactions: list balances: dict # (sender_pubkey, id) tuples used_transaction_ids: set block_number: int persist_address: int valid: bool def from_bytes(block_raw, associated_data = None): assert len(block_raw) == 293 transaction_raw = block_raw[0:149] transaction = transaction_from_bytes(transaction_raw, associated_data) return Block( transaction = transaction, message = block_raw[149:181], miner_pubkey = block_raw[181:213], previous_hash = block_raw[213:245], timestamp = int.from_bytes(block_raw[245:253], "big"), difficulty_sum = int.from_bytes(block_raw[253:285], "big"), nonce = int.from_bytes(block_raw[285:293], "big"), own_hash = hashlib.sha256(block_raw).digest(), pending_commitment_blocks = None, pending_gambling_transactions = None, balances = None, used_transaction_ids = None, block_number = None, persist_address = None, valid = False, ) def validate(self, blockchain): 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 not self.transaction.is_valid_after_block(prev_block): return False else: prev_block = None if not self.transaction.is_empty(): 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_block_number(prev_block) self.calculate_pending_gambling_transactions(prev_block) self.calculate_balances(prev_block) self.calculate_used_transaction_ids(prev_block) self.calculate_persist_address(prev_block) return self.valid def calculate_pending_gambling_transactions(self, prev_block): if prev_block is None: self.pending_commitment_blocks = [] self.pending_gambling_transactions = [] return pending_commitment_blocks = prev_block.pending_commitment_blocks.copy() pending_gambling_transactions = prev_block.pending_gambling_transactions.copy() if isinstance(self.transaction, GamblingTransaction): pending_gambling_transactions.append(self.transaction) if self.block_number % 256 == 255: pending_commitment_blocks.append((self.own_hash, pending_gambling_transactions)) pending_gambling_transactions = [] if isinstance(self.transaction, RevealTransaction): pending_commitment_blocks = pending_commitment_blocks[1:] self.pending_commitment_blocks = pending_commitment_blocks self.pending_gambling_transactions = pending_gambling_transactions 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 isinstance(t, PaymentTransaction): balances[self.miner_pubkey] += t.transaction_fee balances[t.sender] -= (t.amount + t.transaction_fee) balances.setdefault(t.receiver, 0) balances[t.receiver] += t.amount elif isinstance(t, GamblingTransaction): balances[self.miner_pubkey] += t.transaction_fee balances[t.player] -= (t.amount + t.transaction_fee) elif isinstance(t, RevealTransaction): balances[self.miner_pubkey] += 100 balances.setdefault(t.revealer_pubkey, 0) balances[t.revealer_pubkey] += 1500 revealed_gamblings = prev_block.pending_commitment_blocks[0][1] for transaction in revealed_gamblings: to_hash = transaction.id.to_bytes(4, "big") + \ transaction.player + \ t.reveal_value h = hashlib.sha256(to_hash).digest() if h[0] < 0x80: continue balances[transaction.player] += 2 * transaction.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 isinstance(t, PaymentTransaction): used_transaction_ids.add((t.sender, t.id)) elif isinstance(t, GamblingTransaction): used_transaction_ids.add((t.player, 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 calculate_persist_address(self, prev_block): if prev_block is None: self.persist_address = 0 else: self.persist_address = prev_block.persist_address + 293 if isinstance(prev_block.transaction, RevealTransaction): self.persist_address += 5376 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): return self.transaction.get_transaction_raw() + \ 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) < 1024: return True return transaction.sorting_id() < self.__open_transactions[-1].sorting_id() def __cleanup(self): # sort everything self.__open_transactions.sort(key = lambda t: t.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() contains_reveal_transaction = False def is_valid(transaction): nonlocal contains_reveal_transaction if isinstance(transaction, PaymentTransaction): sender_tuple = (transaction.sender, transaction.id) elif isinstance(transaction, GamblingTransaction): sender_tuple = (transaction.player, transaction.id) elif isinstance(transaction, RevealTransaction): latest_block = self.__blockchain.get_latest_block() if latest_block is None: return False if not transaction.is_valid_after_block(latest_block): return False if contains_reveal_transaction: return False contains_reveal_transaction = True return True if sender_tuple in used_transaction_ids: return False balance = balances.get(sender_tuple[0]) or 0 if transaction.amount + transaction.transaction_fee > balance: return False used_transaction_ids.add(sender_tuple) balances[sender_tuple[0]] = 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 = 53 * b"\0" else: transaction = self.__open_transactions[i] transaction_data = transaction.open_transactions_hash_data() 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.__associated_data = {} 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(293) if len(block) < 293: break if associated_data_required(block[0:149]): associated_data = f.read(5376) if len(associated_data) < 5376: break else: associated_data = None block_obj = self.add_block(block, associated_data) 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_addr = block_list[-1].persist_address 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()) if isinstance(block.transaction, RevealTransaction): f.write(block.transaction.get_associated_data()) f.truncate() def add_block(self, block_raw, associated_data = None): with self.__lock: block = Block.from_bytes(block_raw, associated_data) 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] def cache_associated_data(self, transaction): if not isinstance(transaction, RevealTransaction): return with self.__lock: self.__associated_data[transaction.associated_proof_hash] = transaction.get_associated_data() def get_associated_data(self, associated_proof_hash): with self.__lock: return self.__associated_data.get(associated_proof_hash, None)