diff --git a/blockchain.py b/blockchain.py index 43f1ce1..4e9e942 100644 --- a/blockchain.py +++ b/blockchain.py @@ -5,8 +5,23 @@ import hashlib from multiprocessing import Lock import time -@dataclass +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 @@ -15,18 +30,18 @@ class Transaction: 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], + 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 = self.id.to_bytes(4, "big") + \ + msg = b"\x01" + \ + self.id.to_bytes(4, "big") + \ self.sender + \ self.receiver + \ self.amount.to_bytes(8, "big") + \ @@ -44,18 +59,178 @@ class Transaction: return False return balance >= self.amount + self.transaction_fee def get_transaction_raw(self): - return self.id.to_bytes(4, "big") + \ + 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 (-self.transaction_fee, self.sender, self.id) + return (1, -self.transaction_fee, -self.amount, self.sender, self.id) + def is_empty(self): + return False def __eq__(self, other): - return (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \ + 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 @@ -66,37 +241,39 @@ class Block: 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): - 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) + 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[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"), + 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 self.transaction is not None: - if not self.transaction.is_valid(): - return False + 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: @@ -107,11 +284,11 @@ class Block: 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): + if not self.transaction.is_valid_after_block(prev_block): return False else: prev_block = None - if self.transaction is not 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() @@ -127,10 +304,28 @@ class Block: 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_block_number(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 = { @@ -139,13 +334,29 @@ class Block: return balances = prev_block.balances.copy() balances.setdefault(self.miner_pubkey, 0) - balances.setdefault(t.receiver, 0) balances[self.miner_pubkey] += 100 t = self.transaction - if t is not None: + 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: @@ -153,14 +364,23 @@ class Block: return used_transaction_ids = prev_block.used_transaction_ids.copy() t = self.transaction - if t is not None: + 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 @@ -172,11 +392,7 @@ class Block: 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 + \ + return self.transaction.get_transaction_raw() + \ self.message + \ self.miner_pubkey + \ self.previous_hash + \ @@ -216,12 +432,12 @@ class OpenTransactions: return None return self.__open_transactions[i] def __has_space(self, transaction): - if len(self.__open_transactions) < 1000: + 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 = Transaction.sorting_id) + self.__open_transactions.sort(key = lambda t: t.sorting_id()) # drop out invalid ones # - reused ids # - paying more money than available @@ -232,15 +448,30 @@ class OpenTransactions: return used_transaction_ids = latest_block.used_transaction_ids.copy() balances = latest_block.balances.copy() + contains_reveal_transaction = False def is_valid(transaction): - sender_tuple = (transaction.sender, transaction.id) + 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(transaction.sender) or 0 + balance = balances.get(sender_tuple[0]) 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 + 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 @@ -251,12 +482,10 @@ class OpenTransactions: current_hash = 32 * b"\0" for i in range(1024): if i >= len(self.__open_transactions): - transaction_data = 44 * b"\0" + transaction_data = 53 * 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") + transaction_data = transaction.open_transactions_hash_data() current_hash = hashlib.sha256(current_hash + transaction_data).digest() self.__hashes.append(current_hash) @@ -265,6 +494,7 @@ class Blockchain: # 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() @@ -273,10 +503,16 @@ class Blockchain: try: with open("blockchain", "rb") as f: while True: - block = f.read(292) - if len(block) < 292: + block = f.read(293) + if len(block) < 293: break - block_obj = self.add_block(block) + 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 @@ -319,16 +555,18 @@ class Blockchain: 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 + 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()) - def add_block(self, 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) + 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] @@ -347,3 +585,11 @@ class Blockchain: 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) diff --git a/docs/blockchain.md b/docs/blockchain.md index f68bc44..6ae0644 100644 --- a/docs/blockchain.md +++ b/docs/blockchain.md @@ -2,6 +2,8 @@ The carrotcoin cryptocurrency is a bitcoin-like currency with proof-of-work, created just for learning purposes. +A special feature is its builtin option for gambling. + # The currency One carrotcoin (abbreviated cc) consists of 100 cents. Internally, only cent amounts are processed using integer fields. The recommended notation for an amount is like in this example: @@ -93,6 +95,7 @@ If a payment transaction is included, the following happens additionally: If a gambling transaction is included, the following happens additionally: - The player needs to pay "gambling amount" + "transaction fee" +- The miner gets "transaction fee" on top of the reward. If a gambling reveal transaction is included, the following happens additionally: @@ -182,13 +185,13 @@ Technically, gambling happens in the following steps: ## Gambling transaction -A user can create a gambling transaction at any time to participate in the gambling process. Such transactions are described above in the section "Blockchain" / "transaction datastructure" / "gambling transaction". The id and player fields (along with other information) will be used to calculate, if the player wins. +A user can create a gambling transaction at any time to participate in the gambling process. Such transactions are described above in the section "Blockchain" / "transaction datastructure" / "gambling transaction". The id and player fields (along with other information) will be used to calculate if the player wins. ## Gambling commitment blocks To find out which block is a gambling commitment block, you need to number all blocks of the blockchain. The first block (this is the one with 32 nullbytes in the "previous hash" field) gets block number 0. From there on, just count up. (The block number of a block is the block number of its previous block plus one.) -Every block with a block number of 255 (modulo 256) is a gambling commitment block. Its hash value will also be relevant to calculate, if a player wins. +Every block with a block number of 255 (modulo 256) is a gambling commitment block. Its hash value will also be relevant to calculate if a player wins. ## Decision of winning / losing diff --git a/docs/protocol-v0.md b/docs/protocol-v0.md index aa441f9..9577fe5 100644 --- a/docs/protocol-v0.md +++ b/docs/protocol-v0.md @@ -11,6 +11,13 @@ Node to Node communication happens over IPv6 only. When starting a node process, it tries to claim udp port 62039. If this port is not free, any other port is chosen. +### Version fields + +Every packet starts with a 4-byte header containing two version fields. (A 2-byte "protocol version" and a 2-byte "capable version".) +Participants should fill both fields with 0 when sending messages. When receiving a messages with "protocol version" != 0, the message should be ignored. The "capable version" must not be checked for incoming messages, so messages are processed regardless of the value they contain as "capable version". + +This is the current behaviour for all "protocol version 0" participants and should allow to extend the protocol in the future, if this becomes necessary. + ## Node behaviour ### Peers @@ -43,14 +50,19 @@ Each node keeps a list of up to 1024 open transactions. (That are valid but not The list is sorted by the following criteria: - transaction fee, decreasing +- sent or played amount, decreasing - sender pubkey, increasing - id, increasing -These sorting criteria form a 3-tuple. +These sorting criteria form a 4-tuple. -Only one transaction per (sender pubkey, id) tuple stays in the list. If a transaction with the same tuple but greater transaction fee is received, it replaces the current transaction. If the transaction fee is equal or smaller, the new transaction is ignored. +Only one transaction per (sender pubkey, id) tuple stays in the list. If a transaction with the same (sender pubkey, id) tuple but greater transaction fee or same fee but greater amount is received, it replaces the current transaction. In other cases (smaller fee or neither the fee nor the amount increased), the new transaction is ignored. -If one sender created multiple transactions, it must have a large enough balance for all transactions, otherwise the excess ones (as defined by the sorting criteria above) are removed from the list. +By choosing different ids, one sender can put multiple transactions into the list. + +Each sender must have a large enough balance for all transactions, otherwise the excess ones (as defined by the sorting criteria above) are removed from the list. + +At most one (the next pending) gambling reveal transaction can be part of the list and will be the first one, before all other transactions. If the list grows above 1024 entries, a node may either remove excess ones or keep them in a local list. Within the Node to Node communication, only the first 1024 entries will be synced. @@ -60,13 +72,17 @@ The hash of each open transaction is a SHA256, calculated over the following dat | content | length | |---|---| | previous hash | 32 | +| is reveal transaction (bool) | 1 | | transaction fee (BE) | 8 | -| sender pubkey | 32 | +| amount (BE) | 8 | +| sender / player pubkey | 32 | | id | 4 | The "previous hash" consists of 32 nullbytes for the first transaction in the list. For all other entries, it is the calculated hash value of the open transaction entry directly before in the list. -If the list contains less than 1024 entries, set "transaction fee", "sender pubkey" and "id" to nullbytes for the hash calculation of all following entries. +The field "is reveal transaction" is 0x01 for the first transaction if it is a reveal transaction and 0x00 for all other transactions. Reveal transactions have "transaction fee", "amount", "sender / player pubkey" and "id" set to nullbytes. + +If the list contains less than 1024 entries, set "is reveal transaction", "transaction fee", "amount", "sender / player pubkey" and "id" to nullbytes for the hash calculation of all following entries. ## Node to Node message packet formats @@ -76,7 +92,7 @@ If the list contains less than 1024 entries, set "transaction fee", "sender pubk |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 0 (BE) | 1 | +| type = 0 (dec) | 1 | | difficulty sum of second last known block (BE) | 32 | | hash value of open transaction # 1023 | 32 | | partner IPv6 | 16 | @@ -92,7 +108,7 @@ partner IPv6 and partner port may be nullbytes (no partner included). |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 1 (BE) | 1 | +| type = 1 (dec) | 1 | | block hash | 32 | A block request is sent from node A to node B in order to transfer a block from node B to node A. @@ -106,8 +122,8 @@ If "block hash" consists of 32 nullbytes, node A wants node B to send the newest |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 2 (BE) | 1 | -| block | 292 | +| type = 2 (dec) | 1 | +| block | 293 | A "block transfer" message is sent back in response to a "block request" message. @@ -117,7 +133,7 @@ A "block transfer" message is sent back in response to a "block request" message |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 3 (BE) | 1 | +| type = 3 (dec) | 1 | | list position (0 <= x < 1024) (BE) | 2 | ### open transaction list hash response @@ -126,7 +142,7 @@ A "block transfer" message is sent back in response to a "block request" message |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 4 (BE) | 1 | +| type = 4 (dec) | 1 | | list position (0 <= x < 1024) (BE) | 2 | | "open transaction" hash value | 32 | @@ -136,7 +152,7 @@ A "block transfer" message is sent back in response to a "block request" message |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 5 (BE) | 1 | +| type = 5 (dec) | 1 | | list position (0 <= x < 1024) (BE) | 2 | ### open transaction response @@ -145,9 +161,9 @@ A "block transfer" message is sent back in response to a "block request" message |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 6 (BE) | 1 | +| type = 6 (dec) | 1 | | list position (0 <= x < 1024) (BE) | 2 | -| transaction | 148 | +| transaction | 149 | ## Client to Node message packet formats @@ -157,8 +173,8 @@ A "block transfer" message is sent back in response to a "block request" message |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 7 (BE) | 1 | -| padding (nullbytes) | 252 | +| type = 7 (dec) | 1 | +| padding (nullbytes) | 253 | The node should answer to a "Mining task request" with a "Mining task response" @@ -168,8 +184,8 @@ The node should answer to a "Mining task request" with a "Mining task response" |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 8 (BE) | 1 | -| transaction (optional) | 148 | +| type = 8 (dec) | 1 | +| transaction | 149 | | previous hash | 32 | | timestamp (unix time in seconds, BE) | 8 | | difficulty sum (BE) | 32 | @@ -181,23 +197,130 @@ The miner fills "nonce", "message" and "miner pubkey" on its own. When a miner finds a block, it sends a "block transfer" message to the node. -### Payment request (Client -> Node) +### Transaction request (Client -> Node) | content | size (bytes) | |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 9 (BE) | 1 | -| transaction | 148 | +| type = 9 (dec) | 1 | +| transaction | 149 | -The node should answer to a "Payment request" with a "Payment request received" if the contained transaction is formally correct (see "validity / transaction" in the blockchain specification). +The transaction can be of any type. (Payment, gambling, gambling reveal) -A response is always sent back in this case, even if the transaction cannot be applied to the blockchain right now. +The node should answer to a "Transaction request" with a "Transaction request received" if the contained transaction is formally correct (see "validity / transaction" in the blockchain specification). -### Payment request received (Node -> Client) +To validate a gambling reveal transaction, the node will first ask the client for the associated proof (see "Associated revealing proof request" and "Associated revealing proof response" messages) before sending back a "Transaction request received" response. + +A response is always sent back for valid transaction data structures, even if the transaction cannot be applied to the blockchain (e.g. because the sender used the transaction id before or has not enough money). For gambling reveal transactions, the request is confirmed if the proof was received, the proof hash matches and all numbers are correctly in range. (0 < I < n/2) The check if the reveal proof matches the oldest not yet revealed commitment block, is done after confirming with "Transaction request received". + +### Transaction request received (Node -> Client) | content | size (bytes) | |---|---| | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | -| type = 10 (BE) | 1 | +| type = 10 (dec) | 1 | + +### Reveal mining task request (Client -> Node) + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 11 (dec) | 1 | +| padding (nullbytes) | 32 | + +The node should answer to a "Reveal mining task request" with a "Reveal mining task response" + +### Reveal mining task response (Node -> Client) + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 12 (dec) | 1 | +| commitment hash | 32 | + +The commitment hash is either 32 nullbytes ("nothing to do for reveal miners at the moment") or it contains the hash of the oldest not yet revealed gambling commitment block. + +See blockchain.md / section Gambling for the mathematical background what to do with the commitment hash "H". + +When the miner is done forming a proof, he is expected to create a gambling reveal transaction and send it to a node using a "Transaction request" message. It should also be ready to receive and answer "Associated revealing proof request" messages from that node. + +## General packet formats + +These packets can be used in both situations (For Client <-> Node communication and for Node <-> Node communication.) + +### Ping + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 13 (dec) | 1 | +| ASCII string "R u carrotcoin?" | 15 | +| nonce (arbitrary value) | 8 | + +The ASCII string "R u carrotcoin?" is fixed and messages with a different content in this part of the message should be ignored. + +When receiving a valid Ping message, a Pong message with the same nonce value must be sent back to the sender. + +### Pong + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 14 (dec) | 1 | +| ASCII string "I m carrotcoin!" | 15 | +| nonce (same value as in the Ping) | 8 | + +An answer message to a Ping request. See Ping for implementation details. + +### Associated revealing proof request + +As described in the blockchain specification, a reveal transaction requires an "associated revealing proof" to be fully validated. + +There are 3 possible situations when a participant A transfers a reveal transaction to a participant B: + +1. A reveal mining client (A) finished calculating the proof and sends the transaction to some node (B). +2. A node (A) further spreads this transaction to another node (B). +3. A node (A) transfers a block with a reveal transaction to another node (B). + +In each case, if B does not know the associated revealing proof, it needs to ask A for it with the following message: + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 15 (dec) | 1 | +| parts bitfield | 1 | +| associated revealing proof (hash) | 32 | + +The associated revealing proof described in the blockchain has a total length of 5376 bytes. For transmission, it is equally divided into 6 parts with 896 bytes each. These parts are numbered from "part 0" (the first 896 bytes) to "part 5" (the last 896 bytes). + +A sha256 hash value of the entire 5376 byte datastructure is the identifier that is used inside the transaction and used in this request to identify the proof being requested. + +The "parts bitfield" describes which parts should be sent back. 0x01 means "part 0", 0x02 means "part 1", ..., 0x20 means "part 5". To request all 6 parts (which might be a typical request), the "parts bitfield" will have value 0x3f. If a retransmission is needed, the "parts bitfield" can describe exactly, which parts are still missing. + +When node A sent a reveal transation to node B, it should answer such associated revealing proof requests from B with all required parts, each part transmitted as an individual UDP packet of type "Associated revealing proof response". + +### Associated revealing proof response + +| content | size (bytes) | +|---|---| +| protocol version = 0 (BE) | 2 | +| capable version = 0 (BE) | 2 | +| type = 16 (dec) | 1 | +| part number | 1 | +| associated revealing proof (hash) | 32 | +| proof partial data | 896 | + +This message is sent back as response to an associated revealing proof request. See above for implementation details. + +The "part number" describes where this part belongs to and has a value from 0 to 5 (inclusive). + +The "associated revealing proof (hash)" identifies the entire 5376-byte proof and is the same as in the request. + +The "proof partial data" contains the actual information (1/6 of the entire proof in each message). diff --git a/list-block-messages.py b/list-block-messages.py index de837cd..171df2a 100755 --- a/list-block-messages.py +++ b/list-block-messages.py @@ -19,12 +19,15 @@ def main(): try: with open("blockchain", "rb") as f: while True: - block = f.read(292) - if len(block) != 292: + block = f.read(293) + if len(block) != 293: break - timestamp = int.from_bytes(block[244:252], "big") + if block[0] == 3: + # Reveal Transaction, skip the associated data + f.read(5376) + timestamp = int.from_bytes(block[245:253], "big") time_info = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp)) - message = prepare_message(block[148:180]) + message = prepare_message(block[149:181]) print(f"[{time_info}] {message}") except FileNotFoundError: print("Found no blockchain file", file=sys.stderr) diff --git a/list-open-transactions.py b/list-open-transactions.py index 27ddab5..1ed59e9 100755 --- a/list-open-transactions.py +++ b/list-open-transactions.py @@ -9,8 +9,8 @@ def get_transaction(s, i): try: while True: msg, sender = s.recvfrom(4096) - if sender[0:2] == ("::1", 62039) and len(msg) == 155 and msg[0:5] == b"\0\0\0\0\x06" and msg[5:7] == i.to_bytes(2, "big"): - return msg[7:155] + if sender[0:2] == ("::1", 62039) and len(msg) == 156 and msg[0:5] == b"\0\0\0\0\x06" and msg[5:7] == i.to_bytes(2, "big"): + return msg[7:156] except TimeoutError: pass return None @@ -32,13 +32,25 @@ def main(): if transaction is None: print("- no response from local node -", file=sys.stderr) exit(1) - if transaction == 148 * b"\0": + if transaction == 149 * b"\0": return - sender = format_addr(transaction[4:36]) - receiver = format_addr(transaction[36:68]) - amount = format_amount(transaction[68:76]) - fee = format_amount(transaction[76:84]) - print(f"{sender} {receiver} {amount:>13} {fee:>10}") + if transaction[0] == 1: + sender = format_addr(transaction[5:37]) + receiver = format_addr(transaction[37:69]) + amount = format_amount(transaction[69:77]) + fee = format_amount(transaction[77:85]) + elif transaction[0] == 2: + sender = format_addr(transaction[5:37]) + receiver = "(gambling)" + 33 * " " + amount = format_amount(transaction[37:45]) + fee = format_amount(transaction[45:53]) + elif transaction[0] == 3: + print("- reveal transaction -") + sender = None + else: + sender = None + if sender is not None: + print(f"{sender} {receiver} {amount:>13} {fee:>10}") if __name__ == '__main__': main() diff --git a/miner/src/main.rs b/miner/src/main.rs index ee13375..5199834 100644 --- a/miner/src/main.rs +++ b/miner/src/main.rs @@ -131,20 +131,20 @@ fn main() { work_counter: 0, })); let (results_tx, results_rx) = mpsc::channel(); - let mut request = vec![0u8; 257]; + let mut request = vec![0u8; 258]; request[4] = 7; loop { let mut recv_buffer = vec![0u8; 512]; loop { match socket.recv(&mut recv_buffer) { Ok(size) => { - if size == 257 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] { - let transaction = &recv_buffer[5..153]; - let previous_hash = &recv_buffer[153..185]; - let timestamp = &recv_buffer[185..193]; - let difficulty_sum = &recv_buffer[193..225]; - let threshold = &recv_buffer[225..257]; - let mut prefix = Vec::with_capacity(292); + if size == 258 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] { + let transaction = &recv_buffer[5..154]; + let previous_hash = &recv_buffer[154..186]; + let timestamp = &recv_buffer[186..194]; + let difficulty_sum = &recv_buffer[194..226]; + let threshold = &recv_buffer[226..258]; + let mut prefix = Vec::with_capacity(293); prefix.extend_from_slice(transaction); prefix.extend_from_slice(&message); prefix.extend_from_slice(&public_key); diff --git a/node.py b/node.py index 81625dd..e93fa0b 100755 --- a/node.py +++ b/node.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -import blockchain, hashlib, observer, random, socket, sys, threading, time +import blockchain, hashlib, observer, random, secrets, socket, sys, threading, time from _queue import Empty DEFAULT_PORT = 62039 @@ -101,15 +101,14 @@ def send_heartbeat(node, peer, b): node.node_socket.sendto(heartbeat_msg, (peer.ipv6, peer.port)) def define_partnership(peers): + for peer in peers: + peer.partner = None peers_to_pair = [peer for peer in peers if peer.lifetime_counter >= 8] random.shuffle(peers_to_pair) pairing_count = len(peers_to_pair) // 2 for pair_idx in range(pairing_count): peers_to_pair[2*pair_idx].partner = peers_to_pair[2*pair_idx+1] peers_to_pair[2*pair_idx+1].partner = peers_to_pair[2*pair_idx] - # in case of an odd count, the last one will remain without partner - if pairing_count % 2 == 1: - peers_to_pair[-1].partner = None def heartbeat(node, b): while True: @@ -216,6 +215,30 @@ def transfer_block(addr, node, receive_observer, b): except NoReponseException: pass +def get_associated_data(node, receive_observer, b, addr, associated_data_hash): + cached_data = b.get_associated_data(associated_data_hash) + if cached_data is not None: + return cached_data + subscription = receive_observer.listen((addr[0:2], "associated data", associated_data_hash)) + fragments = 6 * [None] + for _ in range(10): + request_bitfield = 0 + for i, fragment in enumerate(fragments): + if fragment is None: + request_bitfield |= (1 << i) + request = b"\0\0\0\0\x0f" + bytes([request_bitfield]) + associated_data_hash + node.node_socket.sendto(request, addr) + try: + while True: + response = subscription.receive(1) + if fragments[response["part_number"]] is None: + fragments[response["part_number"]] = response["fragment"] + if None not in fragments: + return b"".join(fragments) + except Empty: + pass + return None + def compare_open_transactions(addr, node, receive_observer, b): try: cursor = 511 @@ -238,9 +261,16 @@ def compare_open_transactions(addr, node, receive_observer, b): def transaction_condition(response): return response["position"] == cursor remote_transaction = request_retry(node, addr, request, subscription, transaction_condition) - parsed_transaction = blockchain.Transaction.from_bytes(remote_transaction["transaction"]) - if not parsed_transaction.is_valid(): + if blockchain.associated_data_required(remote_transaction["transaction"]): + associated_data = get_associated_data(node, receive_observer, b, addr, remote_transaction["transaction"][33:65]) + if associated_data is None: + return + else: + associated_data = None + parsed_transaction = blockchain.transaction_from_bytes(remote_transaction["transaction"], associated_data) + if not parsed_transaction.is_valid() or parsed_transaction.is_empty(): return + b.cache_associated_data(parsed_transaction) b.open_transactions.add(parsed_transaction) except NoReponseException: pass @@ -252,7 +282,7 @@ def receiver(node, b): sender = describe(addr[0], addr[1]) msg_len = len(msg) if msg_len < 4: - log("Got a udp message from {sender} that was too short.") + log(f"Got a udp message from {sender} that was too short.") continue version = int.from_bytes(msg[0:2], "big") if version != 0: @@ -300,16 +330,26 @@ def receiver(node, b): node.node_socket.sendto(response_msg, addr) elif msg_type == 2: # block transfer - if msg_len != 297: - log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 297 bytes)") + if msg_len != 298: + log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 298 bytes)") continue - block_raw = msg[5:297] - new_block = b.add_block(block_raw) - block_hash = new_block.own_hash - if new_block.validate(b) and b.set_latest_block(block_hash): - log("Got a new block") - identifier = (addr[0:2], "block transfer") - receive_observer.publish(identifier, block_hash) + def handle_block_transfer(addr, msg): + if blockchain.associated_data_required(msg[5:154]): + associated_data = get_associated_data(node, receive_observer, b, addr, msg[38:70]) + if associated_data is None: + return + else: + associated_data = None + new_block = b.add_block(msg[5:298], associated_data) + block_hash = new_block.own_hash + if new_block.validate(b): + b.cache_associated_data(new_block.transaction) + if b.set_latest_block(block_hash): + log("Got a new block") + identifier = (addr[0:2], "block transfer") + receive_observer.publish(identifier, block_hash) + # Handle this in a thread because asynchronous back-requests might be required + threading.Thread(target=handle_block_transfer, args=(addr, msg)).start() elif msg_type == 3: # open transaction list hash request if msg_len != 7: @@ -344,32 +384,32 @@ def receiver(node, b): continue transaction = b.open_transactions.get_transaction(list_position) if transaction is None: - transaction_raw = 148 * b"\0" + transaction_raw = 149 * b"\0" else: transaction_raw = transaction.get_transaction_raw() response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw node.node_socket.sendto(response, addr) elif msg_type == 6: # open transaction response - if msg_len != 155: - log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 155 bytes)") + if msg_len != 156: + log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 156 bytes)") continue event_obj = { "position": int.from_bytes(msg[5:7], "big"), - "transaction": msg[7:155], + "transaction": msg[7:156], } identifier = (addr[0:2], "transaction") receive_observer.publish(identifier, event_obj) elif msg_type == 7: # mining task request - if msg_len != 257: - log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 257 bytes)") + if msg_len != 258: + log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 258 bytes)") continue transaction = b.open_transactions.get_transaction(0) if transaction is not None: transaction_raw = transaction.get_transaction_raw() else: - transaction_raw = 148 * b"\0" + transaction_raw = 149 * b"\0" t = int(time.time()) timestamp_raw = t.to_bytes(8, "big") latest_block = b.get_latest_block() @@ -395,15 +435,106 @@ def receiver(node, b): threshold.to_bytes(32, "big") node.node_socket.sendto(response, addr) elif msg_type == 9: - # payment request - if msg_len != 153: - log(f"Got a payment of wrong length ({msg_len} bytes from {sender}, but expected 153 bytes)") + # transaction request + if msg_len != 154: + log(f"Got a transaction of wrong length ({msg_len} bytes from {sender}, but expected 154 bytes)") continue - parsed_transaction = blockchain.Transaction.from_bytes(msg[5:153]) - if not parsed_transaction.is_valid(): + def handle_transaction_request(msg, addr): + if blockchain.associated_data_required(msg[5:154]): + associated_data = get_associated_data(node, receive_observer, b, addr, msg[38:70]) + if associated_data is None: + return + else: + associated_data = None + parsed_transaction = blockchain.transaction_from_bytes(msg[5:154], associated_data) + if not parsed_transaction.is_valid() or parsed_transaction.is_empty(): + return + b.cache_associated_data(parsed_transaction) + b.open_transactions.add(parsed_transaction) + node.node_socket.sendto(b"\0\0\0\0\x0a", addr) + # Handle this in a thread because asynchronous back-requests might be required + threading.Thread(target=handle_transaction_request, args=(msg, addr)).start() + elif msg_type == 11: + # reveal mining task request + if msg_len != 37: + log(f"Got a reveal mining task request of wrong length ({msg_len} bytes from {sender}, but expected 37 bytes)") continue - b.open_transactions.add(parsed_transaction) - node.node_socket.sendto(b"\0\0\0\0\x0a", addr) + next_reveal_hash = 32 * b"\0" + latest_block = b.get_latest_block() + if latest_block is not None and len(latest_block.pending_commitment_blocks) > 0: + next_reveal_hash = latest_block.pending_commitment_blocks[0][0] + node.node_socket.sendto(b"\0\0\0\0\x0c" + next_reveal_hash, addr) + elif msg_type == 13: + # Ping + if msg_len != 28: + log(f"Got a Ping message of wrong length ({msg_len} bytes from {sender}, but expected 28 bytes)") + continue + if msg[5:20] != "R u carrotcoin?".encode(): + # Wrong Ping question, ignore + continue + nonce = msg[20:28] + node.node_socket.sendto(b"\0\0\0\0\x0e" + "I m carrotcoin!".encode() + nonce, addr) + elif msg_type == 14: + # Pong + if msg_len != 28: + log(f"Got a Pong message of wrong length ({msg_len} bytes from {sender}, but expected 28 bytes)") + continue + if msg[5:20] != "I m carrotcoin!".encode(): + # Wrong Pong answer, ignore + continue + nonce = msg[20:28] + identifier = (addr[0:2], "pong") + receive_observer.publish(identifier, nonce) + elif msg_type == 15: + # Associated revealing proof request + if msg_len != 38: + log(f"Got an associated revealing proof request of wrong length ({msg_len} bytes from {sender}, but expected exactly 38 bytes)") + continue + parts_bitfield = msg[5] + associated_proof_hash = msg[6:38] + proof = b.get_associated_data(associated_proof_hash) + if proof is None: + # Cannot send anything, proof is unknown + continue + def handle_associated_relvealing_proof_request(addr, parts_bitfield, associated_proof_hash, proof): + # Ping first, otherwise this protocol part would be a really strong ddos reflection amplifier + subscription = receive_observer.listen((addr[0:2], "pong")) + nonce = secrets.randbits(64).to_bytes(8, "big") + ping = b"\0\0\0\0\x0dR u carrotcoin?" + nonce + node.node_socket.sendto(ping, addr[0:2]) + try: + while True: + pong_nonce = subscription.receive(5) + if pong_nonce == nonce: + break + except Empty: + # No response to ping, ignore the initial request + return + for part_number in range(6): + if parts_bitfield & (1 << part_number) == 0: + continue + revealing_proof_response = b"\0\0\0\0\x10" + \ + bytes([part_number]) + \ + associated_proof_hash + \ + proof[part_number*896:(part_number+1)*896] + node.node_socket.sendto(revealing_proof_response, addr[0:2]) + # Start a thread because of the asynchronous ping + threading.Thread(target=handle_associated_relvealing_proof_request, args=(addr, parts_bitfield, associated_proof_hash, proof)).start() + elif msg_type == 16: + # Associated revealing proof response + if msg_len != 934: + log(f"Got an associated revealing proof response of wrong length ({msg_len} bytes from {sender}, but expected exactly 934 bytes)") + continue + part_number = msg[5] + if part_number not in range(6): + continue + associated_data_hash = msg[6:38] + fragment = msg[38:934] + identifier = (addr[0:2], "associated data", associated_data_hash) + receive_observer.publish(identifier, { + "part_number": part_number, + "fragment": fragment + }) else: log(f"Got a udp message of unknown type from {sender}. (type {msg_type})") diff --git a/reveal-miner/.gitignore b/reveal-miner/.gitignore new file mode 100644 index 0000000..ce74c5c --- /dev/null +++ b/reveal-miner/.gitignore @@ -0,0 +1 @@ +/progress diff --git a/reveal-miner/mine.py b/reveal-miner/mine.py new file mode 100755 index 0000000..5d758e8 --- /dev/null +++ b/reveal-miner/mine.py @@ -0,0 +1,195 @@ +#! /usr/bin/env python3 + +import base64, hashlib, os, queue, socket, sys, threading, time + +n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143 + +def log(msg): + time_info = time.strftime("%d.%m.%Y %H:%M:%S") + print(f"[{time_info}] {msg}") + +def load_progress(): + try: + f = open("progress", "r+b") + except FileNotFoundError: + f = open("progress", "w+b") + return [f, 0, 0, 0] + length = f.seek(0, os.SEEK_END) + if length < 256: + return [f, 0, 0, 0] + f.seek(0) + begin = int.from_bytes(f.read(256), "big") + position = length // 256 - 1 + f.seek(256 * position) + current_value = int.from_bytes(f.read(256), "big") + return [f, begin, position, current_value] + +def normalize(x): + if x > n // 2: + return n - x + return x + +def reduce_milestones(a, e): + half_length = len(a) // 2 + return [(a[i] * pow(a[half_length+i], e, n)) % n for i in range(half_length+1)] + +def finalize(f, revealer_pubkey): + f.seek(0) + milestones = [int.from_bytes(f.read(256), "big") for _ in range(2**15 + 1)] + final_R = normalize(milestones[-1]) + intermediates = [] + for i in range(30, 10, -1): + H = normalize(milestones[0]) + R = normalize(milestones[-1]) + if i > 15: + I = normalize(milestones[len(milestones) // 2]) + else: + I = normalize(pow(H, 2**2**(i-1), n)) + milestones = [milestones[0], I, milestones[-1]] + hash_data = revealer_pubkey + \ + H.to_bytes(256, "big") + \ + I.to_bytes(256, "big") + \ + R.to_bytes(256, "big") + \ + i.to_bytes(1, "big") + hash_value = hashlib.sha256(hash_data).digest() + e = int.from_bytes(hash_value, "big") + milestones = reduce_milestones(milestones, e) + intermediates.append(I) + proof_numbers = [final_R] + intermediates + return [x.to_bytes(256, "big") for x in proof_numbers] + +def send_transaction(revealer_pubkey, proof): + proof_block = b"".join(proof) + associated_proof_hash = hashlib.sha256(proof_block).digest() + transaction = b"\x03" + revealer_pubkey + associated_proof_hash + 84 * b"\0" + + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.connect(("::1", 62039)) + + attempts = 10 + next_attempt = time.time() + while True: + timeout = next_attempt - time.time() + if timeout > 0: + s.settimeout(timeout) + try: + msg = s.recv(4096) + if len(msg) == 38 and msg[0:2] == b"\0\0" and msg[4] == 15 and msg[6:38] == associated_proof_hash: + bitfield = msg[5] + for bitpos in range(6): + if (bitfield & (1 << bitpos)) != 0: + begin = 896 * bitpos + end = 896 * (bitpos + 1) + reply = b"\0\0\0\0\x10" + bytes([bitpos]) + associated_proof_hash + proof_block[begin:end] + s.send(reply) + elif len(msg) == 5 and msg[0:2] == b"\0\0" and msg[4] == 10: + return True + except TimeoutError: + pass + elif attempts == 0: + return False + else: + attempts -= 1 + next_attempt += 1.0 + transaction_request = b"\0\0\0\0\x09" + transaction + s.send(transaction_request) + +def mining_loop(event_queue, revealer_pubkey): + step_exponent = 2**2**15 + f, begin, position, value = load_progress() + last_status = position * 100 // 2**15 + proof = None + sent = False + def reset(): + nonlocal f, begin, position, value, last_status, proof, sent + f.seek(0) + f.truncate() + if begin != 0: + f.write(begin.to_bytes(256, "big")) + position = 0 + last_status = 0 + value = begin + proof = None + sent = False + while True: + if begin == 0: + begin = event_queue.get() + reset() + continue + try: + new_begin = event_queue.get(0) + if new_begin != begin: + begin = new_begin + if begin == 0: + log("No task right now") + else: + log("Got a new revealing task") + reset() + continue + except queue.Empty: + pass + if position < 2**15: + value = pow(value, step_exponent, n) + f.write(value.to_bytes(256, "big")) + position += 1 + new_status = position * 100 // 2**15 + if new_status != last_status: + log(f"{new_status} %") + last_status = new_status + elif proof is None: + log("finalizing") + proof = finalize(f, revealer_pubkey) + elif not sent: + log("Sending reveal transaction") + sent = send_transaction(revealer_pubkey, proof) + if sent: + log("Transaction has been received by our node") + else: + log("(no response from the node)") + else: + new_begin = event_queue.get() + if new_begin == begin: + continue + begin = new_begin + if begin == 0: + log("No task right now") + else: + log("Got a new revealing task") + reset() + +# communicate with node, send change events +def main(): + if len(sys.argv) < 2: + print("Usage: ./mine.py ", file=sys.stderr) + exit(1) + + revealer_pubkey = base64.b64decode(sys.argv[1]) + assert len(revealer_pubkey) == 32 + + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + s.connect(("::1", 62039)) + s.settimeout(1) + + event_queue = None + + while True: + request = b"\0\0\0\0\x0b" + 32 * b"\0" + s.send(request) + try: + response = s.recv(4096) + if len(response) == 37 and response[0:5] == b"\0\0\0\0\x0c": + begin = int.from_bytes(response[5:37], "big") + if event_queue is None: + event_queue = queue.Queue() + threading.Thread(target=mining_loop, args=(event_queue, revealer_pubkey)).start() + if begin == 0: + log("node is available, waiting for a reveal mining task") + else: + log("node is available, starting reveal mining process") + event_queue.put(begin) + time.sleep(1) + except TimeoutError: + log("Got no response from the node") + +if __name__ == '__main__': + main() diff --git a/wallet.py b/wallet.py index f7ef292..6367d90 100755 --- a/wallet.py +++ b/wallet.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -import base64, socket, sys, time +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 @@ -27,33 +27,75 @@ def write_transaction(timestamp, message, amount): def show_balance(public_key): public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw) - with open("blockchain", "rb") as f: - total_amount = 0 - while True: - block = f.read(292) - if len(block) != 292: - break - miner = block[180:212] - timestamp = int.from_bytes(block[244:252], "big") - if block[0:148] != 148 * b"\0": - sender = block[4:36] - receiver = block[36:68] - amount = int.from_bytes(block[68:76], "big") - fee = int.from_bytes(block[76:84], "big") - if sender == public_key_raw: - write_transaction(timestamp, format_address(receiver), - amount - fee) - total_amount -= (amount + fee) - if receiver == public_key_raw: - write_transaction(timestamp, format_address(sender), amount) - total_amount += amount - else: - fee = 0 - if miner == public_key_raw: - write_transaction(timestamp, "mining reward", 100 + fee) - total_amount += 100 + fee - 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}") + 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(",", ".") @@ -61,10 +103,12 @@ def parse_amount(amount): if len(parts) == 1: return int(parts[0]) * 100 elif len(parts) == 2: - if len(parts[1]) > 2: + if len(parts[1]) == 0 or len(parts[1]) > 2: raise Exception(f"Invalid amount: {amount}") - coins = int(parts[0]) - cents = int(parts[1]) + 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}") @@ -81,14 +125,17 @@ def find_free_id(public_key_raw): with open("blockchain", "rb") as f: used_ids = set() while True: - block = f.read(292) - if len(block) != 292: + block = f.read(293) + if len(block) != 293: break - if block[0:148] != 148 * b"\0": - transaction_id = int.from_bytes(block[0:4], "big") - sender = block[4:36] + 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 @@ -106,15 +153,34 @@ def send_payment(private_key, target, amount, fee): 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 = transaction_id.to_bytes(4, "big") + \ + 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 - request = b"\0\0\0\0\x09" + transaction + 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)) @@ -136,6 +202,7 @@ 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: @@ -156,6 +223,8 @@ def main(): 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)