Implement gambling

This commit is contained in:
2025-12-13 23:41:07 +01:00
parent fd6b5019ac
commit 872e44e317
10 changed files with 954 additions and 171 deletions

View File

@@ -5,8 +5,23 @@ import hashlib
from multiprocessing import Lock from multiprocessing import Lock
import time import time
@dataclass n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143
class Transaction: 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 id: int
sender: bytes sender: bytes
receiver: bytes receiver: bytes
@@ -15,18 +30,18 @@ class Transaction:
signature: bytes signature: bytes
def from_bytes(transaction_raw): def from_bytes(transaction_raw):
assert len(transaction_raw) == 148 return PaymentTransaction(
return Transaction( id = int.from_bytes(transaction_raw[1:5], "big"),
id = int.from_bytes(transaction_raw[0:4], "big"), sender = transaction_raw[5:37],
sender = transaction_raw[4:36], receiver = transaction_raw[37:69],
receiver = transaction_raw[36:68], amount = int.from_bytes(transaction_raw[69:77], "big"),
amount = int.from_bytes(transaction_raw[68:76], "big"), transaction_fee = int.from_bytes(transaction_raw[77:85], "big"),
transaction_fee = int.from_bytes(transaction_raw[76:84], "big"), signature = transaction_raw[85:149],
signature = transaction_raw[84:148],
) )
def is_valid(self): def is_valid(self):
sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender) 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.sender + \
self.receiver + \ self.receiver + \
self.amount.to_bytes(8, "big") + \ self.amount.to_bytes(8, "big") + \
@@ -44,18 +59,178 @@ class Transaction:
return False return False
return balance >= self.amount + self.transaction_fee return balance >= self.amount + self.transaction_fee
def get_transaction_raw(self): def get_transaction_raw(self):
return self.id.to_bytes(4, "big") + \ return b"\x01" + \
self.id.to_bytes(4, "big") + \
self.sender + \ self.sender + \
self.receiver + \ self.receiver + \
self.amount.to_bytes(8, "big") + \ self.amount.to_bytes(8, "big") + \
self.transaction_fee.to_bytes(8, "big") + \ self.transaction_fee.to_bytes(8, "big") + \
self.signature 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): 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): 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) (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 @dataclass
class Block: class Block:
nonce: int nonce: int
@@ -66,35 +241,37 @@ class Block:
miner_pubkey: bytes miner_pubkey: bytes
transaction: Transaction transaction: Transaction
own_hash: bytes own_hash: bytes
pending_commitment_blocks: list
pending_gambling_transactions: list
balances: dict balances: dict
# (sender_pubkey, id) tuples # (sender_pubkey, id) tuples
used_transaction_ids: set used_transaction_ids: set
block_number: int block_number: int
persist_address: int
valid: bool valid: bool
def from_bytes(block_raw): def from_bytes(block_raw, associated_data = None):
assert len(block_raw) == 292 assert len(block_raw) == 293
transaction_raw = block_raw[0:148] transaction_raw = block_raw[0:149]
if transaction_raw == 148 * b"\0": transaction = transaction_from_bytes(transaction_raw, associated_data)
transaction = None
else:
transaction = Transaction.from_bytes(transaction_raw)
return Block( return Block(
transaction = transaction, transaction = transaction,
message = block_raw[148:180], message = block_raw[149:181],
miner_pubkey = block_raw[180:212], miner_pubkey = block_raw[181:213],
previous_hash = block_raw[212:244], previous_hash = block_raw[213:245],
timestamp = int.from_bytes(block_raw[244:252], "big"), timestamp = int.from_bytes(block_raw[245:253], "big"),
difficulty_sum = int.from_bytes(block_raw[252:284], "big"), difficulty_sum = int.from_bytes(block_raw[253:285], "big"),
nonce = int.from_bytes(block_raw[284:292], "big"), nonce = int.from_bytes(block_raw[285:293], "big"),
own_hash = hashlib.sha256(block_raw).digest(), own_hash = hashlib.sha256(block_raw).digest(),
pending_commitment_blocks = None,
pending_gambling_transactions = None,
balances = None, balances = None,
used_transaction_ids = None, used_transaction_ids = None,
block_number = None, block_number = None,
persist_address = None,
valid = False, valid = False,
) )
def validate(self, blockchain): def validate(self, blockchain):
if self.transaction is not None:
if not self.transaction.is_valid(): if not self.transaction.is_valid():
return False return False
if self.previous_hash != 32 * b"\0": if self.previous_hash != 32 * b"\0":
@@ -107,11 +284,11 @@ class Block:
return False return False
if self.timestamp > time.time(): if self.timestamp > time.time():
return False 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 return False
else: else:
prev_block = None prev_block = None
if self.transaction is not None: if not self.transaction.is_empty():
return False return False
# check for the correct miner pubkey - which will become public at launch day # check for the correct miner pubkey - which will become public at launch day
h = hashlib.sha256(self.miner_pubkey).hexdigest() h = hashlib.sha256(self.miner_pubkey).hexdigest()
@@ -127,10 +304,28 @@ class Block:
return False return False
self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256 self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256
if self.valid: if self.valid:
self.calculate_block_number(prev_block)
self.calculate_pending_gambling_transactions(prev_block)
self.calculate_balances(prev_block) self.calculate_balances(prev_block)
self.calculate_used_transaction_ids(prev_block) self.calculate_used_transaction_ids(prev_block)
self.calculate_block_number(prev_block) self.calculate_persist_address(prev_block)
return self.valid 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): def calculate_balances(self, prev_block):
if prev_block is None: if prev_block is None:
self.balances = { self.balances = {
@@ -139,13 +334,29 @@ class Block:
return return
balances = prev_block.balances.copy() balances = prev_block.balances.copy()
balances.setdefault(self.miner_pubkey, 0) balances.setdefault(self.miner_pubkey, 0)
balances.setdefault(t.receiver, 0)
balances[self.miner_pubkey] += 100 balances[self.miner_pubkey] += 100
t = self.transaction t = self.transaction
if t is not None: if isinstance(t, PaymentTransaction):
balances[self.miner_pubkey] += t.transaction_fee balances[self.miner_pubkey] += t.transaction_fee
balances[t.sender] -= (t.amount + t.transaction_fee) balances[t.sender] -= (t.amount + t.transaction_fee)
balances.setdefault(t.receiver, 0)
balances[t.receiver] += t.amount 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 self.balances = balances
def calculate_used_transaction_ids(self, prev_block): def calculate_used_transaction_ids(self, prev_block):
if prev_block is None: if prev_block is None:
@@ -153,14 +364,23 @@ class Block:
return return
used_transaction_ids = prev_block.used_transaction_ids.copy() used_transaction_ids = prev_block.used_transaction_ids.copy()
t = self.transaction t = self.transaction
if t is not None: if isinstance(t, PaymentTransaction):
used_transaction_ids.add((t.sender, t.id)) 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 self.used_transaction_ids = used_transaction_ids
def calculate_block_number(self, prev_block): def calculate_block_number(self, prev_block):
if prev_block is None: if prev_block is None:
self.block_number = 0 self.block_number = 0
else: else:
self.block_number = prev_block.block_number + 1 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): def get_difficulty_info(self, steps, blockchain):
if steps == 0: if steps == 0:
return self.difficulty_sum, self.timestamp return self.difficulty_sum, self.timestamp
@@ -172,11 +392,7 @@ class Block:
previous_block = blockchain.get_block(self.previous_hash) previous_block = blockchain.get_block(self.previous_hash)
return previous_block.get_difficulty_info(steps-1, blockchain) return previous_block.get_difficulty_info(steps-1, blockchain)
def get_block_raw(self): def get_block_raw(self):
if self.transaction is None: return self.transaction.get_transaction_raw() + \
transaction = 148 * b"\0"
else:
transaction = self.transaction.get_transaction_raw()
return transaction + \
self.message + \ self.message + \
self.miner_pubkey + \ self.miner_pubkey + \
self.previous_hash + \ self.previous_hash + \
@@ -216,12 +432,12 @@ class OpenTransactions:
return None return None
return self.__open_transactions[i] return self.__open_transactions[i]
def __has_space(self, transaction): def __has_space(self, transaction):
if len(self.__open_transactions) < 1000: if len(self.__open_transactions) < 1024:
return True return True
return transaction.sorting_id() < self.__open_transactions[-1].sorting_id() return transaction.sorting_id() < self.__open_transactions[-1].sorting_id()
def __cleanup(self): def __cleanup(self):
# sort everything # sort everything
self.__open_transactions.sort(key = Transaction.sorting_id) self.__open_transactions.sort(key = lambda t: t.sorting_id())
# drop out invalid ones # drop out invalid ones
# - reused ids # - reused ids
# - paying more money than available # - paying more money than available
@@ -232,15 +448,30 @@ class OpenTransactions:
return return
used_transaction_ids = latest_block.used_transaction_ids.copy() used_transaction_ids = latest_block.used_transaction_ids.copy()
balances = latest_block.balances.copy() balances = latest_block.balances.copy()
contains_reveal_transaction = False
def is_valid(transaction): def is_valid(transaction):
nonlocal contains_reveal_transaction
if isinstance(transaction, PaymentTransaction):
sender_tuple = (transaction.sender, transaction.id) 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: if sender_tuple in used_transaction_ids:
return False return False
balance = balances.get(transaction.sender) or 0 balance = balances.get(sender_tuple[0]) or 0
if transaction.amount + transaction.transaction_fee > balance: if transaction.amount + transaction.transaction_fee > balance:
return False return False
used_transaction_ids.add(sender_tuple) 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 return True
self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)] self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)]
# limit to 1024 # limit to 1024
@@ -251,12 +482,10 @@ class OpenTransactions:
current_hash = 32 * b"\0" current_hash = 32 * b"\0"
for i in range(1024): for i in range(1024):
if i >= len(self.__open_transactions): if i >= len(self.__open_transactions):
transaction_data = 44 * b"\0" transaction_data = 53 * b"\0"
else: else:
transaction = self.__open_transactions[i] transaction = self.__open_transactions[i]
transaction_data = transaction.transaction_fee.to_bytes(8, "big") + \ transaction_data = transaction.open_transactions_hash_data()
transaction.sender + \
transaction.id.to_bytes(4, "big")
current_hash = hashlib.sha256(current_hash + transaction_data).digest() current_hash = hashlib.sha256(current_hash + transaction_data).digest()
self.__hashes.append(current_hash) self.__hashes.append(current_hash)
@@ -265,6 +494,7 @@ class Blockchain:
# maps block hashes to block instances # maps block hashes to block instances
self.__block_map = {} self.__block_map = {}
self.__latest_block_hash = None self.__latest_block_hash = None
self.__associated_data = {}
self.__lock = Lock() self.__lock = Lock()
self.open_transactions = OpenTransactions(self) self.open_transactions = OpenTransactions(self)
self.__load_blocks_from_disk() self.__load_blocks_from_disk()
@@ -273,10 +503,16 @@ class Blockchain:
try: try:
with open("blockchain", "rb") as f: with open("blockchain", "rb") as f:
while True: while True:
block = f.read(292) block = f.read(293)
if len(block) < 292: if len(block) < 293:
break 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): if not block_obj.validate(self):
break break
last_valid = block_obj last_valid = block_obj
@@ -319,16 +555,18 @@ class Blockchain:
new_block = self.__block_map[new_block.previous_hash] new_block = self.__block_map[new_block.previous_hash]
if old_block is not None and old_block.block_number > new_block.block_number: if old_block is not None and old_block.block_number > new_block.block_number:
old_block = self.__block_map[old_block.previous_hash] old_block = self.__block_map[old_block.previous_hash]
start_block_number = block_list[-1].block_number start_addr = block_list[-1].persist_address
start_addr = start_block_number * 292
open_mode = "wb" if start_addr == 0 else "r+b" open_mode = "wb" if start_addr == 0 else "r+b"
with open("blockchain", open_mode) as f: with open("blockchain", open_mode) as f:
f.seek(start_addr) f.seek(start_addr)
for block in reversed(block_list): for block in reversed(block_list):
f.write(block.get_block_raw()) 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: 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: if block.own_hash not in self.__block_map:
self.__block_map[block.own_hash] = block self.__block_map[block.own_hash] = block
return self.__block_map[block.own_hash] return self.__block_map[block.own_hash]
@@ -347,3 +585,11 @@ class Blockchain:
if self.__latest_block_hash is None: if self.__latest_block_hash is None:
return None return None
return self.__block_map[self.__latest_block_hash] 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)

View File

@@ -2,6 +2,8 @@
The carrotcoin cryptocurrency is a bitcoin-like currency with proof-of-work, created just for learning purposes. 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 # 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: 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: If a gambling transaction is included, the following happens additionally:
- The player needs to pay "gambling amount" + "transaction fee" - 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: If a gambling reveal transaction is included, the following happens additionally:
@@ -182,13 +185,13 @@ Technically, gambling happens in the following steps:
## Gambling transaction ## 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 ## 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.) 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 ## Decision of winning / losing

View File

@@ -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. 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 ## Node behaviour
### Peers ### 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: The list is sorted by the following criteria:
- transaction fee, decreasing - transaction fee, decreasing
- sent or played amount, decreasing
- sender pubkey, increasing - sender pubkey, increasing
- id, 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. 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 | | content | length |
|---|---| |---|---|
| previous hash | 32 | | previous hash | 32 |
| is reveal transaction (bool) | 1 |
| transaction fee (BE) | 8 | | transaction fee (BE) | 8 |
| sender pubkey | 32 | | amount (BE) | 8 |
| sender / player pubkey | 32 |
| id | 4 | | 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. 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 ## 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 | | protocol version = 0 (BE) | 2 |
| capable 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 | | difficulty sum of second last known block (BE) | 32 |
| hash value of open transaction # 1023 | 32 | | hash value of open transaction # 1023 | 32 |
| partner IPv6 | 16 | | partner IPv6 | 16 |
@@ -92,7 +108,7 @@ partner IPv6 and partner port may be nullbytes (no partner included).
|---|---| |---|---|
| protocol version = 0 (BE) | 2 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 1 (BE) | 1 | | type = 1 (dec) | 1 |
| block hash | 32 | | 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. 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 2 (BE) | 1 | | type = 2 (dec) | 1 |
| block | 292 | | block | 293 |
A "block transfer" message is sent back in response to a "block request" message. 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 3 (BE) | 1 | | type = 3 (dec) | 1 |
| list position (0 <= x < 1024) (BE) | 2 | | list position (0 <= x < 1024) (BE) | 2 |
### open transaction list hash response ### 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 4 (BE) | 1 | | type = 4 (dec) | 1 |
| list position (0 <= x < 1024) (BE) | 2 | | list position (0 <= x < 1024) (BE) | 2 |
| "open transaction" hash value | 32 | | "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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 5 (BE) | 1 | | type = 5 (dec) | 1 |
| list position (0 <= x < 1024) (BE) | 2 | | list position (0 <= x < 1024) (BE) | 2 |
### open transaction response ### 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 6 (BE) | 1 | | type = 6 (dec) | 1 |
| list position (0 <= x < 1024) (BE) | 2 | | list position (0 <= x < 1024) (BE) | 2 |
| transaction | 148 | | transaction | 149 |
## Client to Node message packet formats ## 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 7 (BE) | 1 | | type = 7 (dec) | 1 |
| padding (nullbytes) | 252 | | padding (nullbytes) | 253 |
The node should answer to a "Mining task request" with a "Mining task response" 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 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 8 (BE) | 1 | | type = 8 (dec) | 1 |
| transaction (optional) | 148 | | transaction | 149 |
| previous hash | 32 | | previous hash | 32 |
| timestamp (unix time in seconds, BE) | 8 | | timestamp (unix time in seconds, BE) | 8 |
| difficulty sum (BE) | 32 | | 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. 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) | | content | size (bytes) |
|---|---| |---|---|
| protocol version = 0 (BE) | 2 | | protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 |
| type = 9 (BE) | 1 | | type = 9 (dec) | 1 |
| transaction | 148 | | 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) | | content | size (bytes) |
|---|---| |---|---|
| protocol version = 0 (BE) | 2 | | protocol version = 0 (BE) | 2 |
| capable 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).

View File

@@ -19,12 +19,15 @@ def main():
try: try:
with open("blockchain", "rb") as f: with open("blockchain", "rb") as f:
while True: while True:
block = f.read(292) block = f.read(293)
if len(block) != 292: if len(block) != 293:
break 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)) 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}") print(f"[{time_info}] {message}")
except FileNotFoundError: except FileNotFoundError:
print("Found no blockchain file", file=sys.stderr) print("Found no blockchain file", file=sys.stderr)

View File

@@ -9,8 +9,8 @@ def get_transaction(s, i):
try: try:
while True: while True:
msg, sender = s.recvfrom(4096) 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"): 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:155] return msg[7:156]
except TimeoutError: except TimeoutError:
pass pass
return None return None
@@ -32,12 +32,24 @@ def main():
if transaction is None: if transaction is None:
print("- no response from local node -", file=sys.stderr) print("- no response from local node -", file=sys.stderr)
exit(1) exit(1)
if transaction == 148 * b"\0": if transaction == 149 * b"\0":
return return
sender = format_addr(transaction[4:36]) if transaction[0] == 1:
receiver = format_addr(transaction[36:68]) sender = format_addr(transaction[5:37])
amount = format_amount(transaction[68:76]) receiver = format_addr(transaction[37:69])
fee = format_amount(transaction[76:84]) 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}") print(f"{sender} {receiver} {amount:>13} {fee:>10}")
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -131,20 +131,20 @@ fn main() {
work_counter: 0, work_counter: 0,
})); }));
let (results_tx, results_rx) = mpsc::channel(); let (results_tx, results_rx) = mpsc::channel();
let mut request = vec![0u8; 257]; let mut request = vec![0u8; 258];
request[4] = 7; request[4] = 7;
loop { loop {
let mut recv_buffer = vec![0u8; 512]; let mut recv_buffer = vec![0u8; 512];
loop { loop {
match socket.recv(&mut recv_buffer) { match socket.recv(&mut recv_buffer) {
Ok(size) => { Ok(size) => {
if size == 257 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] { if size == 258 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] {
let transaction = &recv_buffer[5..153]; let transaction = &recv_buffer[5..154];
let previous_hash = &recv_buffer[153..185]; let previous_hash = &recv_buffer[154..186];
let timestamp = &recv_buffer[185..193]; let timestamp = &recv_buffer[186..194];
let difficulty_sum = &recv_buffer[193..225]; let difficulty_sum = &recv_buffer[194..226];
let threshold = &recv_buffer[225..257]; let threshold = &recv_buffer[226..258];
let mut prefix = Vec::with_capacity(292); let mut prefix = Vec::with_capacity(293);
prefix.extend_from_slice(transaction); prefix.extend_from_slice(transaction);
prefix.extend_from_slice(&message); prefix.extend_from_slice(&message);
prefix.extend_from_slice(&public_key); prefix.extend_from_slice(&public_key);

181
node.py
View File

@@ -1,6 +1,6 @@
#! /usr/bin/env python3 #! /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 from _queue import Empty
DEFAULT_PORT = 62039 DEFAULT_PORT = 62039
@@ -101,15 +101,14 @@ def send_heartbeat(node, peer, b):
node.node_socket.sendto(heartbeat_msg, (peer.ipv6, peer.port)) node.node_socket.sendto(heartbeat_msg, (peer.ipv6, peer.port))
def define_partnership(peers): def define_partnership(peers):
for peer in peers:
peer.partner = None
peers_to_pair = [peer for peer in peers if peer.lifetime_counter >= 8] peers_to_pair = [peer for peer in peers if peer.lifetime_counter >= 8]
random.shuffle(peers_to_pair) random.shuffle(peers_to_pair)
pairing_count = len(peers_to_pair) // 2 pairing_count = len(peers_to_pair) // 2
for pair_idx in range(pairing_count): 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].partner = peers_to_pair[2*pair_idx+1]
peers_to_pair[2*pair_idx+1].partner = peers_to_pair[2*pair_idx] 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): def heartbeat(node, b):
while True: while True:
@@ -216,6 +215,30 @@ def transfer_block(addr, node, receive_observer, b):
except NoReponseException: except NoReponseException:
pass 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): def compare_open_transactions(addr, node, receive_observer, b):
try: try:
cursor = 511 cursor = 511
@@ -238,9 +261,16 @@ def compare_open_transactions(addr, node, receive_observer, b):
def transaction_condition(response): def transaction_condition(response):
return response["position"] == cursor return response["position"] == cursor
remote_transaction = request_retry(node, addr, request, subscription, transaction_condition) remote_transaction = request_retry(node, addr, request, subscription, transaction_condition)
parsed_transaction = blockchain.Transaction.from_bytes(remote_transaction["transaction"]) if blockchain.associated_data_required(remote_transaction["transaction"]):
if not parsed_transaction.is_valid(): associated_data = get_associated_data(node, receive_observer, b, addr, remote_transaction["transaction"][33:65])
if associated_data is None:
return 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) b.open_transactions.add(parsed_transaction)
except NoReponseException: except NoReponseException:
pass pass
@@ -252,7 +282,7 @@ def receiver(node, b):
sender = describe(addr[0], addr[1]) sender = describe(addr[0], addr[1])
msg_len = len(msg) msg_len = len(msg)
if msg_len < 4: 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 continue
version = int.from_bytes(msg[0:2], "big") version = int.from_bytes(msg[0:2], "big")
if version != 0: if version != 0:
@@ -300,16 +330,26 @@ def receiver(node, b):
node.node_socket.sendto(response_msg, addr) node.node_socket.sendto(response_msg, addr)
elif msg_type == 2: elif msg_type == 2:
# block transfer # block transfer
if msg_len != 297: if msg_len != 298:
log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 297 bytes)") log(f"Got a block transfer of wrong length ({msg_len} bytes from {sender}, but expected 298 bytes)")
continue continue
block_raw = msg[5:297] def handle_block_transfer(addr, msg):
new_block = b.add_block(block_raw) 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 block_hash = new_block.own_hash
if new_block.validate(b) and b.set_latest_block(block_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") log("Got a new block")
identifier = (addr[0:2], "block transfer") identifier = (addr[0:2], "block transfer")
receive_observer.publish(identifier, block_hash) 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: elif msg_type == 3:
# open transaction list hash request # open transaction list hash request
if msg_len != 7: if msg_len != 7:
@@ -344,32 +384,32 @@ def receiver(node, b):
continue continue
transaction = b.open_transactions.get_transaction(list_position) transaction = b.open_transactions.get_transaction(list_position)
if transaction is None: if transaction is None:
transaction_raw = 148 * b"\0" transaction_raw = 149 * b"\0"
else: else:
transaction_raw = transaction.get_transaction_raw() transaction_raw = transaction.get_transaction_raw()
response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw
node.node_socket.sendto(response, addr) node.node_socket.sendto(response, addr)
elif msg_type == 6: elif msg_type == 6:
# open transaction response # open transaction response
if msg_len != 155: if msg_len != 156:
log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 155 bytes)") log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 156 bytes)")
continue continue
event_obj = { event_obj = {
"position": int.from_bytes(msg[5:7], "big"), "position": int.from_bytes(msg[5:7], "big"),
"transaction": msg[7:155], "transaction": msg[7:156],
} }
identifier = (addr[0:2], "transaction") identifier = (addr[0:2], "transaction")
receive_observer.publish(identifier, event_obj) receive_observer.publish(identifier, event_obj)
elif msg_type == 7: elif msg_type == 7:
# mining task request # mining task request
if msg_len != 257: if msg_len != 258:
log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 257 bytes)") log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 258 bytes)")
continue continue
transaction = b.open_transactions.get_transaction(0) transaction = b.open_transactions.get_transaction(0)
if transaction is not None: if transaction is not None:
transaction_raw = transaction.get_transaction_raw() transaction_raw = transaction.get_transaction_raw()
else: else:
transaction_raw = 148 * b"\0" transaction_raw = 149 * b"\0"
t = int(time.time()) t = int(time.time())
timestamp_raw = t.to_bytes(8, "big") timestamp_raw = t.to_bytes(8, "big")
latest_block = b.get_latest_block() latest_block = b.get_latest_block()
@@ -395,15 +435,106 @@ def receiver(node, b):
threshold.to_bytes(32, "big") threshold.to_bytes(32, "big")
node.node_socket.sendto(response, addr) node.node_socket.sendto(response, addr)
elif msg_type == 9: elif msg_type == 9:
# payment request # transaction request
if msg_len != 153: if msg_len != 154:
log(f"Got a payment of wrong length ({msg_len} bytes from {sender}, but expected 153 bytes)") 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():
continue continue
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) b.open_transactions.add(parsed_transaction)
node.node_socket.sendto(b"\0\0\0\0\x0a", addr) 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
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: else:
log(f"Got a udp message of unknown type from {sender}. (type {msg_type})") log(f"Got a udp message of unknown type from {sender}. (type {msg_type})")

1
reveal-miner/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/progress

195
reveal-miner/mine.py Executable file
View File

@@ -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 <wallet address>", 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()

111
wallet.py
View File

@@ -1,6 +1,6 @@
#! /usr/bin/env python3 #! /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.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PrivateFormat, PublicFormat, NoEncryption 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): def show_balance(public_key):
public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw) public_key_raw = public_key.public_bytes(Encoding.Raw, PublicFormat.Raw)
try:
with open("blockchain", "rb") as f: with open("blockchain", "rb") as f:
total_amount = 0 total_amount = 0
block_counter = 0
pending_bets = [[]]
while True: while True:
block = f.read(292) block = f.read(293)
if len(block) != 292: if len(block) != 293:
break break
miner = block[180:212] miner = block[181:213]
timestamp = int.from_bytes(block[244:252], "big") timestamp = int.from_bytes(block[245:253], "big")
if block[0:148] != 148 * b"\0": if block[0] == 1:
sender = block[4:36] # Payment
receiver = block[36:68] sender = block[5:37]
amount = int.from_bytes(block[68:76], "big") receiver = block[37:69]
fee = int.from_bytes(block[76:84], "big") amount = int.from_bytes(block[69:77], "big")
fee = int.from_bytes(block[77:85], "big")
if sender == public_key_raw: if sender == public_key_raw:
write_transaction(timestamp, format_address(receiver), - amount - fee) write_transaction(timestamp, format_address(receiver), - amount)
if fee > 0:
write_transaction(timestamp, 39 * " " + "(fee)", - fee)
total_amount -= (amount + fee) total_amount -= (amount + fee)
if receiver == public_key_raw: if receiver == public_key_raw:
write_transaction(timestamp, format_address(sender), amount) write_transaction(timestamp, format_address(sender), amount)
total_amount += 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: else:
fee = 0 fee = 0
if miner == public_key_raw: if miner == public_key_raw:
write_transaction(timestamp, "mining reward", 100 + fee) write_transaction(timestamp, "mining reward", 100 + fee)
total_amount += 100 + fee total_amount += 100 + fee
if block_counter % 256 == 255:
pending_bets.append([])
block_counter += 1
print(81 * "\u2500") print(81 * "\u2500")
amount_string = f"\U0001f955 \x1b[1;37m{format_amount(total_amount, 41, False)}\x1b[0m" 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}") 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): def parse_amount(amount):
amount = amount.replace(",", ".") amount = amount.replace(",", ".")
@@ -61,10 +103,12 @@ def parse_amount(amount):
if len(parts) == 1: if len(parts) == 1:
return int(parts[0]) * 100 return int(parts[0]) * 100
elif len(parts) == 2: 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}") raise Exception(f"Invalid amount: {amount}")
coins = int(parts[0]) coins = int(parts[0]) if parts[0] != "" else 0
cents = int(parts[1]) cents = int(parts[1]) if parts[1] != "" else 0
if len(parts[1]) == 1:
cents *= 10
return coins * 100 + cents return coins * 100 + cents
raise Exception(f"Invalid amount: {amount}") raise Exception(f"Invalid amount: {amount}")
@@ -81,14 +125,17 @@ def find_free_id(public_key_raw):
with open("blockchain", "rb") as f: with open("blockchain", "rb") as f:
used_ids = set() used_ids = set()
while True: while True:
block = f.read(292) block = f.read(293)
if len(block) != 292: if len(block) != 293:
break break
if block[0:148] != 148 * b"\0": transaction_type = block[0]
transaction_id = int.from_bytes(block[0:4], "big") if transaction_type in (1, 2):
sender = block[4:36] transaction_id = int.from_bytes(block[1:5], "big")
sender = block[5:37]
if sender == public_key_raw: if sender == public_key_raw:
used_ids.add(transaction_id) used_ids.add(transaction_id)
elif transaction_type == 3:
f.read(5376)
for possible_id in range(0, 2**32): for possible_id in range(0, 2**32):
if possible_id not in used_ids: if possible_id not in used_ids:
return possible_id return possible_id
@@ -106,15 +153,34 @@ def send_payment(private_key, target, amount, fee):
fee = parse_amount_checked(fee) fee = parse_amount_checked(fee)
public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) public_key_raw = private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
transaction_id = find_free_id(public_key_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 + \ public_key_raw + \
target_raw + \ target_raw + \
amount.to_bytes(8, "big") + \ amount.to_bytes(8, "big") + \
fee.to_bytes(8, "big") fee.to_bytes(8, "big")
signature = private_key.sign(transaction_prefix) signature = private_key.sign(transaction_prefix)
transaction = transaction_prefix + signature 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) s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
try: try:
s.connect(("::1", 62039)) s.connect(("::1", 62039))
@@ -136,6 +202,7 @@ def usage_info():
print("Usage:", file=sys.stderr) print("Usage:", file=sys.stderr)
print(" ./wallet.py # see your past transactions and balance", file=sys.stderr) print(" ./wallet.py # see your past transactions and balance", file=sys.stderr)
print(" ./wallet.py pay <target> <amount> <fee> # send carrotcoins to someone else", file=sys.stderr) print(" ./wallet.py pay <target> <amount> <fee> # send carrotcoins to someone else", file=sys.stderr)
print(" ./wallet.py gamble <amount> <fee> # set an arbitrary amount on a 50:50 bet", file=sys.stderr)
def main(): def main():
try: try:
@@ -156,6 +223,8 @@ def main():
show_balance(public_key) show_balance(public_key)
elif len(sys.argv) == 5 and sys.argv[1] == "pay": elif len(sys.argv) == 5 and sys.argv[1] == "pay":
send_payment(private_key, *sys.argv[2:5]) 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: else:
usage_info() usage_info()
exit(1) exit(1)