#! /usr/bin/env python3 import base64, hashlib, socket, sys, time from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption def format_address(raw_address): return base64.b64encode(raw_address).decode() def format_amount(amount, width, show_plus = True): color_prefix = "" color_suffix = "" sign = "+ " if show_plus else "" if amount < 0: color_prefix = "\x1b[31m" color_suffix = "\x1b[0m" sign = "- " amount = -amount coins = amount / 100 amount_str = f"{sign}{coins:.02f}" amount_str = f"{amount_str:>{width}}" return f"{color_prefix}{amount_str} cc{color_suffix}" def write_transaction(timestamp, message, amount): formatted_time = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp)) print(f"{formatted_time} {message:<44} {format_amount(amount, 11)}") def show_balance(public_key): public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw) try: with open("blockchain", "rb") as f: total_amount = 0 block_counter = 0 pending_bets = [[]] while True: block = f.read(293) if len(block) != 293: break miner = block[181:213] timestamp = int.from_bytes(block[245:253], "big") if block[0] == 1: # Payment sender = block[5:37] receiver = block[37:69] amount = int.from_bytes(block[69:77], "big") fee = int.from_bytes(block[77:85], "big") if sender == public_key_raw: write_transaction(timestamp, format_address(receiver), - amount) if fee > 0: write_transaction(timestamp, 39 * " " + "(fee)", - fee) total_amount -= (amount + fee) if receiver == public_key_raw: write_transaction(timestamp, format_address(sender), amount) total_amount += amount elif block[0] == 2: # Gambling transaction_id = block[1:5] player = block[5:37] amount = int.from_bytes(block[37:45], "big") fee = int.from_bytes(block[45:53], "big") if player == public_key_raw: write_transaction(timestamp, "- gambling -", -amount) if fee > 0: write_transaction(timestamp, 39 * " " + "(fee)", - fee) total_amount -= (amount + fee) pending_bets[-1].append((transaction_id, amount)) elif block[0] == 3: # Reveal transaction reveal_info = f.read(5376) revealer = block[1:33] R = reveal_info[0:256] relevant_bets = pending_bets[0] pending_bets = pending_bets[1:] fee = 100 for (transaction_id, gambling_amount) in relevant_bets: to_hash = transaction_id + public_key_raw + R if hashlib.sha256(to_hash).digest()[0] >= 0x80: write_transaction(timestamp, f"Won gambling with {format_amount(gambling_amount, 0, False)}", 2 * gambling_amount) total_amount += 2 * gambling_amount else: write_transaction(timestamp, f"Lost gambling with {format_amount(gambling_amount, 0, False)}", 0) if revealer == public_key_raw: write_transaction(timestamp, "revealing reward", 1500) total_amount += 1500 else: fee = 0 if miner == public_key_raw: write_transaction(timestamp, "mining reward", 100 + fee) total_amount += 100 + fee if block_counter % 256 == 255: pending_bets.append([]) block_counter += 1 print(81 * "\u2500") amount_string = f"\U0001f955 \x1b[1;37m{format_amount(total_amount, 41, False)}\x1b[0m" print(21 * " " + f"\x1b[1;37mYour balance:\x1b[0m{amount_string}") except FileNotFoundError: print("File \"blockchain\" not found.\nThis wallet script requires a running node with at least 1 block in the current directory.", file=sys.stderr) exit(1) def parse_amount(amount): amount = amount.replace(",", ".") parts = amount.split(".") if len(parts) == 1: return int(parts[0]) * 100 elif len(parts) == 2: if len(parts[1]) == 0 or len(parts[1]) > 2: raise Exception(f"Invalid amount: {amount}") coins = int(parts[0]) if parts[0] != "" else 0 cents = int(parts[1]) if parts[1] != "" else 0 if len(parts[1]) == 1: cents *= 10 return coins * 100 + cents raise Exception(f"Invalid amount: {amount}") def parse_amount_checked(amount): amount = parse_amount(amount) if amount < 0: raise Exception("amount must not be negative") if amount >= 2**64: raise Exception("amount is too large") return amount def find_free_id(public_key_raw): try: with open("blockchain", "rb") as f: used_ids = set() while True: block = f.read(293) if len(block) != 293: break transaction_type = block[0] if transaction_type in (1, 2): transaction_id = int.from_bytes(block[1:5], "big") sender = block[5:37] if sender == public_key_raw: used_ids.add(transaction_id) elif transaction_type == 3: f.read(5376) for possible_id in range(0, 2**32): if possible_id not in used_ids: return possible_id raise Exception("No transaction id available") except FileNotFoundError: return 0 def send_payment(private_key, target, amount, fee): target_raw = base64.b64decode(target.encode()) if len(target_raw) != 32: raise Exception(f"Invalid target address: {target}") amount = parse_amount_checked(amount) if amount == 0: raise Exception("Amount must not be zero") fee = parse_amount_checked(fee) public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) transaction_id = find_free_id(public_key_raw) transaction_prefix = b"\x01" + \ transaction_id.to_bytes(4, "big") + \ public_key_raw + \ target_raw + \ amount.to_bytes(8, "big") + \ fee.to_bytes(8, "big") signature = private_key.sign(transaction_prefix) transaction = transaction_prefix + signature send_transaction(transaction) def gamble(private_key, amount, fee): amount = parse_amount_checked(amount) if amount == 0: raise Exception("Amount must not be zero") fee = parse_amount_checked(fee) public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) transaction_id = find_free_id(public_key_raw) transaction_prefix = b"\x02" + \ transaction_id.to_bytes(4, "big") + \ public_key_raw + \ amount.to_bytes(8, "big") + \ fee.to_bytes(8, "big") signature = private_key.sign(transaction_prefix) transaction = transaction_prefix + signature + 32 * b"\0" send_transaction(transaction) def send_transaction(transaction): request = b"\0\0\0\0\x09" + transaction s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) try: s.connect(("::1", 62039)) s.settimeout(1) for _ in range(10): s.send(request) try: response = s.recv(1024) if response == b"\0\0\0\0\x0a": print("Payment has been transmitted to the node") return except TimeoutError: pass print("node did not respond") except ConnectionRefusedError: print("node is not running") def usage_info(): print("Usage:", file=sys.stderr) print(" ./wallet.py # see your past transactions and balance", file=sys.stderr) print(" ./wallet.py pay # send carrotcoins to someone else", file=sys.stderr) print(" ./wallet.py gamble # set an arbitrary amount on a 50:50 bet", file=sys.stderr) def main(): try: with open("wallet-key", "rb") as f: private_key_raw = f.read() private_key = Ed25519PrivateKey.from_private_bytes(private_key_raw) except FileNotFoundError: private_key = Ed25519PrivateKey.generate() private_key_raw = private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()) with open("wallet-key", "wb") as f: f.write(private_key_raw) public_key = private_key.public_key() wallet_addr = format_address(public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)) print(f"Your wallet address: {wallet_addr}\n") if len(sys.argv) == 1: show_balance(public_key) elif len(sys.argv) == 5 and sys.argv[1] == "pay": send_payment(private_key, *sys.argv[2:5]) elif len(sys.argv) == 4 and sys.argv[1] == "gamble": gamble(private_key, *sys.argv[2:4]) else: usage_info() exit(1) if __name__ == '__main__': main()