Files
carrotcoin/blockchain.py
Lukas Fürderer 0aa58b137b Fix a bug that prevented the blockchain to sync
Blocks with reveal transactions that were loaded from disk could not be
distributed to other nodes before. This prevented new nodes with an old
blockchain state from synchronizing the blockchain.

This bug has now been fixed.
2026-03-22 17:47:42 +01:00

597 lines
25 KiB
Python

from dataclasses import dataclass
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
import hashlib
from multiprocessing import Lock
import time
n = 22152137184697602751949152182712806144286735269755991212091578018969267364854563246965161398309375840488156997640625085213985729183180764348127989435514689722834129288342499703533613231239853168289771769536412763137391054558055082146752229593979328251181647873233949602834141648160681711983351545692646009424518816069561938917629523175464947983950548802679152115205735609960641453864298194702935993896839374645356040490091081577992299773430144650589605043643969140352237968606446474316247592579560197155686719175897498255683642121505357781103123719079205647707696181709515150954235402701095586525936356219917713227143
class Transaction:
pass
class NoTransaction(Transaction):
def is_valid(self):
return True
def is_valid_after_block(self, block):
return True
def get_transaction_raw(self):
return 149 * b"\0"
def is_empty(self):
return True
@dataclass
class PaymentTransaction(Transaction):
id: int
sender: bytes
receiver: bytes
amount: int
transaction_fee: int
signature: bytes
def from_bytes(transaction_raw):
return PaymentTransaction(
id = int.from_bytes(transaction_raw[1:5], "big"),
sender = transaction_raw[5:37],
receiver = transaction_raw[37:69],
amount = int.from_bytes(transaction_raw[69:77], "big"),
transaction_fee = int.from_bytes(transaction_raw[77:85], "big"),
signature = transaction_raw[85:149],
)
def is_valid(self):
sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender)
msg = b"\x01" + \
self.id.to_bytes(4, "big") + \
self.sender + \
self.receiver + \
self.amount.to_bytes(8, "big") + \
self.transaction_fee.to_bytes(8, "big")
try:
sender_pubkey.verify(self.signature, msg)
except InvalidSignature:
return False
return self.amount >= 1
def is_valid_after_block(self, block):
if (self.sender, self.id) in block.used_transaction_ids:
return False
balance = block.balances.get(self.sender)
if balance is None:
return False
return balance >= self.amount + self.transaction_fee
def get_transaction_raw(self):
return b"\x01" + \
self.id.to_bytes(4, "big") + \
self.sender + \
self.receiver + \
self.amount.to_bytes(8, "big") + \
self.transaction_fee.to_bytes(8, "big") + \
self.signature
def open_transactions_hash_data(self):
return b"\0" + \
self.transaction_fee.to_bytes(8, "big") + \
self.amount.to_bytes(8, "big") + \
self.sender + \
self.id.to_bytes(4, "big")
def sorting_id(self):
return (1, -self.transaction_fee, -self.amount, self.sender, self.id)
def is_empty(self):
return False
def __eq__(self, other):
return isinstance(other, PaymentTransaction) \
and (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \
(other.id, other.sender, other.receiver, other.amount, other.transaction_fee)
@dataclass
class GamblingTransaction(Transaction):
id: int
player: bytes
amount: int
transaction_fee: int
signature: bytes
def from_bytes(transaction_raw):
if transaction_raw[117:149] != 32 * b"\0":
return InvalidTransaction()
return GamblingTransaction(
id = int.from_bytes(transaction_raw[1:5], "big"),
player = transaction_raw[5:37],
amount = int.from_bytes(transaction_raw[37:45], "big"),
transaction_fee = int.from_bytes(transaction_raw[45:53], "big"),
signature = transaction_raw[53:117],
)
def is_valid(self):
player_pubkey = Ed25519PublicKey.from_public_bytes(self.player)
msg = b"\x02" + \
self.id.to_bytes(4, "big") + \
self.player + \
self.amount.to_bytes(8, "big") + \
self.transaction_fee.to_bytes(8, "big")
try:
player_pubkey.verify(self.signature, msg)
except InvalidSignature:
return False
return self.amount >= 1
def is_valid_after_block(self, block):
if (self.player, self.id) in block.used_transaction_ids:
return False
balance = block.balances.get(self.player)
if balance is None:
return False
return balance >= self.amount + self.transaction_fee
def get_transaction_raw(self):
return b"\x02" + \
self.id.to_bytes(4, "big") + \
self.player + \
self.amount.to_bytes(8, "big") + \
self.transaction_fee.to_bytes(8, "big") + \
self.signature + \
32 * b"\0"
def open_transactions_hash_data(self):
return b"\0" + \
self.transaction_fee.to_bytes(8, "big") + \
self.amount.to_bytes(8, "big") + \
self.player + \
self.id.to_bytes(4, "big")
def sorting_id(self):
return (1, -self.transaction_fee, -self.amount, self.player, self.id)
def is_empty(self):
return False
def __eq__(self, other):
return isinstance(other, GamblingTransaction) \
and (self.id, self.player, self.amount, self.transaction_fee) == \
(other.id, other.player, other.amount, other.transaction_fee)
def is_valid_group_element(x):
return x > 0 and x <= n//2
def normalize(x):
if x <= n//2:
return x
return n - x
@dataclass
class RevealTransaction:
revealer_pubkey: bytes
associated_proof_hash: bytes
reveal_value: bytes
intermediates: list
def from_bytes(transaction_raw, associated_proof_data):
if transaction_raw[65:149] != 84 * b"\0":
return InvalidTransaction()
return RevealTransaction(
revealer_pubkey = transaction_raw[1:33],
associated_proof_hash = transaction_raw[33:65],
reveal_value = associated_proof_data[0:256],
intermediates = [associated_proof_data[(i+1)*256:(i+2)*256] for i in range(20)],
)
def is_valid(self):
to_hash = self.get_associated_data()
return hashlib.sha256(to_hash).digest() == self.associated_proof_hash
def is_valid_after_block(self, block):
if len(block.pending_commitment_blocks) == 0:
return False
claim_R = int.from_bytes(self.reveal_value, "big")
if not is_valid_group_element(claim_R):
return False
intermediate_numbers = [int.from_bytes(i, "big") for i in self.intermediates]
for intermediate in intermediate_numbers:
if not is_valid_group_element(intermediate):
return False
claim_H = int.from_bytes(block.pending_commitment_blocks[0][0], "big")
for c in range(20):
i = 30 - c
claim_I = intermediate_numbers[c]
to_hash = self.revealer_pubkey + \
claim_H.to_bytes(256, "big") + \
claim_I.to_bytes(256, "big") + \
claim_R.to_bytes(256, "big") + \
i.to_bytes(1, "big")
e = int.from_bytes(hashlib.sha256(to_hash).digest(), "big")
new_H = normalize((claim_H * pow(claim_I, e, n)) % n)
new_R = normalize((claim_I * pow(claim_R, e, n)) % n)
claim_H = new_H
claim_R = new_R
return normalize(pow(claim_H, 2**1024, n)) == claim_R
def get_transaction_raw(self):
return b"\x03" + \
self.revealer_pubkey + \
self.associated_proof_hash + \
84 * b"\0"
def open_transactions_hash_data(self):
return b"\x01" + 52 * b"\0"
def get_associated_data(self):
return b"".join([self.reveal_value] + self.intermediates)
def sorting_id(self):
return (0,)
def is_empty(self):
return False
def __eq__(self, other):
return isinstance(other, RevealTransaction) \
and (self.revealer_pubkey, self.associated_proof_hash, self.reveal_value, self.intermediates) == \
(other.revealer_pubkey, other.associated_proof_hash, other.reveal_value, other.intermediates)
class InvalidTransaction:
def is_valid(self):
return False
def associated_data_required(transaction):
return transaction[0] == 3 and transaction[65:149] == 84 * b"\0"
def transaction_from_bytes(transaction_raw, associated_data = None):
assert len(transaction_raw) == 149
if transaction_raw == 149 * b"\0":
return NoTransaction()
elif transaction_raw[0] == 1:
return PaymentTransaction.from_bytes(transaction_raw)
elif transaction_raw[0] == 2:
return GamblingTransaction.from_bytes(transaction_raw)
elif transaction_raw[0] == 3:
return RevealTransaction.from_bytes(transaction_raw, associated_data)
else:
return InvalidTransaction()
@dataclass
class Block:
nonce: int
timestamp: int
previous_hash: bytes
message: bytes
difficulty_sum: int
miner_pubkey: bytes
transaction: Transaction
own_hash: bytes
pending_commitment_blocks: list
pending_gambling_transactions: list
balances: dict
# (sender_pubkey, id) tuples
used_transaction_ids: set
block_number: int
persist_address: int
valid: bool
def from_bytes(block_raw, associated_data = None):
assert len(block_raw) == 293
transaction_raw = block_raw[0:149]
transaction = transaction_from_bytes(transaction_raw, associated_data)
return Block(
transaction = transaction,
message = block_raw[149:181],
miner_pubkey = block_raw[181:213],
previous_hash = block_raw[213:245],
timestamp = int.from_bytes(block_raw[245:253], "big"),
difficulty_sum = int.from_bytes(block_raw[253:285], "big"),
nonce = int.from_bytes(block_raw[285:293], "big"),
own_hash = hashlib.sha256(block_raw).digest(),
pending_commitment_blocks = None,
pending_gambling_transactions = None,
balances = None,
used_transaction_ids = None,
block_number = None,
persist_address = None,
valid = False,
)
def validate(self, blockchain):
if not self.transaction.is_valid():
return False
if self.previous_hash != 32 * b"\0":
prev_block = blockchain.get_block(self.previous_hash)
if prev_block is None:
return False
if not prev_block.valid:
return False
if self.timestamp <= prev_block.timestamp:
return False
if self.timestamp > time.time():
return False
if not self.transaction.is_valid_after_block(prev_block):
return False
else:
prev_block = None
if not self.transaction.is_empty():
return False
# check for the correct miner pubkey - which will become public at launch day
h = hashlib.sha256(self.miner_pubkey).hexdigest()
if h != "88023d392db35f2d3936abd0532003ae0a38b4d35e4d123a0fa28c568c7e3e2f":
return False
B_1_difficulty_sum, _ = self.get_difficulty_info(1, blockchain)
B_10_difficulty_sum, B_10_timestamp = self.get_difficulty_info(10, blockchain)
D = B_1_difficulty_sum - B_10_difficulty_sum
T = self.timestamp - B_10_timestamp
calculated_difficulty = D * 3000 // 9 // T
block_difficulty = max(calculated_difficulty, 2**28)
if B_1_difficulty_sum + block_difficulty != self.difficulty_sum:
return False
self.valid = int.from_bytes(self.own_hash, "big") * block_difficulty < 2**256
if self.valid:
self.calculate_block_number(prev_block)
self.calculate_pending_gambling_transactions(prev_block)
self.calculate_balances(prev_block)
self.calculate_used_transaction_ids(prev_block)
self.calculate_persist_address(prev_block)
return self.valid
def calculate_pending_gambling_transactions(self, prev_block):
if prev_block is None:
self.pending_commitment_blocks = []
self.pending_gambling_transactions = []
return
pending_commitment_blocks = prev_block.pending_commitment_blocks.copy()
pending_gambling_transactions = prev_block.pending_gambling_transactions.copy()
if isinstance(self.transaction, GamblingTransaction):
pending_gambling_transactions.append(self.transaction)
if self.block_number % 256 == 255:
pending_commitment_blocks.append((self.own_hash, pending_gambling_transactions))
pending_gambling_transactions = []
if isinstance(self.transaction, RevealTransaction):
pending_commitment_blocks = pending_commitment_blocks[1:]
self.pending_commitment_blocks = pending_commitment_blocks
self.pending_gambling_transactions = pending_gambling_transactions
def calculate_balances(self, prev_block):
if prev_block is None:
self.balances = {
self.miner_pubkey: 100,
}
return
balances = prev_block.balances.copy()
balances.setdefault(self.miner_pubkey, 0)
balances[self.miner_pubkey] += 100
t = self.transaction
if isinstance(t, PaymentTransaction):
balances[self.miner_pubkey] += t.transaction_fee
balances[t.sender] -= (t.amount + t.transaction_fee)
balances.setdefault(t.receiver, 0)
balances[t.receiver] += t.amount
elif isinstance(t, GamblingTransaction):
balances[self.miner_pubkey] += t.transaction_fee
balances[t.player] -= (t.amount + t.transaction_fee)
elif isinstance(t, RevealTransaction):
balances[self.miner_pubkey] += 100
balances.setdefault(t.revealer_pubkey, 0)
balances[t.revealer_pubkey] += 1500
revealed_gamblings = prev_block.pending_commitment_blocks[0][1]
for transaction in revealed_gamblings:
to_hash = transaction.id.to_bytes(4, "big") + \
transaction.player + \
t.reveal_value
h = hashlib.sha256(to_hash).digest()
if h[0] < 0x80:
continue
balances[transaction.player] += 2 * transaction.amount
self.balances = balances
def calculate_used_transaction_ids(self, prev_block):
if prev_block is None:
self.used_transaction_ids = set()
return
used_transaction_ids = prev_block.used_transaction_ids.copy()
t = self.transaction
if isinstance(t, PaymentTransaction):
used_transaction_ids.add((t.sender, t.id))
elif isinstance(t, GamblingTransaction):
used_transaction_ids.add((t.player, t.id))
self.used_transaction_ids = used_transaction_ids
def calculate_block_number(self, prev_block):
if prev_block is None:
self.block_number = 0
else:
self.block_number = prev_block.block_number + 1
def calculate_persist_address(self, prev_block):
if prev_block is None:
self.persist_address = 0
else:
self.persist_address = prev_block.persist_address + 293
if isinstance(prev_block.transaction, RevealTransaction):
self.persist_address += 5376
def get_difficulty_info(self, steps, blockchain):
if steps == 0:
return self.difficulty_sum, self.timestamp
if self.previous_hash == 32 * b"\0":
difficulty_sum = 2**29 - steps * 2**28
timestamp = self.timestamp - steps * 300
return difficulty_sum, timestamp
else:
previous_block = blockchain.get_block(self.previous_hash)
return previous_block.get_difficulty_info(steps-1, blockchain)
def get_block_raw(self):
return self.transaction.get_transaction_raw() + \
self.message + \
self.miner_pubkey + \
self.previous_hash + \
self.timestamp.to_bytes(8, "big") + \
self.difficulty_sum.to_bytes(32, "big") + \
self.nonce.to_bytes(8, "big")
class OpenTransactions:
def __init__(self, blockchain):
self.__blockchain = blockchain
self.__open_transactions = []
self.__lock = Lock()
self.__recalculate_hashes()
def add(self, transaction):
assert transaction.is_valid()
with self.__lock:
# pre-check 1: Check for duplicates
for existing_transaction in self.__open_transactions:
if transaction == existing_transaction:
return
# pre-check 2: Check if there is space
if not self.__has_space(transaction):
return
# Add the transaction
self.__open_transactions.append(transaction)
self.__cleanup()
def update(self):
with self.__lock:
self.__cleanup()
def get_hash(self, i):
assert i < 1024
with self.__lock:
return self.__hashes[i]
def get_transaction(self, i):
with self.__lock:
if i >= len(self.__open_transactions):
return None
return self.__open_transactions[i]
def __has_space(self, transaction):
if len(self.__open_transactions) < 1024:
return True
return transaction.sorting_id() < self.__open_transactions[-1].sorting_id()
def __cleanup(self):
# sort everything
self.__open_transactions.sort(key = lambda t: t.sorting_id())
# drop out invalid ones
# - reused ids
# - paying more money than available
latest_block = self.__blockchain.get_latest_block()
if latest_block is None:
self.__open_transactions = []
self.__recalculate_hashes()
return
used_transaction_ids = latest_block.used_transaction_ids.copy()
balances = latest_block.balances.copy()
contains_reveal_transaction = False
def is_valid(transaction):
nonlocal contains_reveal_transaction
if isinstance(transaction, PaymentTransaction):
sender_tuple = (transaction.sender, transaction.id)
elif isinstance(transaction, GamblingTransaction):
sender_tuple = (transaction.player, transaction.id)
elif isinstance(transaction, RevealTransaction):
latest_block = self.__blockchain.get_latest_block()
if latest_block is None:
return False
if not transaction.is_valid_after_block(latest_block):
return False
if contains_reveal_transaction:
return False
contains_reveal_transaction = True
return True
if sender_tuple in used_transaction_ids:
return False
balance = balances.get(sender_tuple[0]) or 0
if transaction.amount + transaction.transaction_fee > balance:
return False
used_transaction_ids.add(sender_tuple)
balances[sender_tuple[0]] = balance - transaction.amount - transaction.transaction_fee
return True
self.__open_transactions = [transaction for transaction in self.__open_transactions if is_valid(transaction)]
# limit to 1024
self.__open_transactions = self.__open_transactions[0:1024]
self.__recalculate_hashes()
def __recalculate_hashes(self):
self.__hashes = []
current_hash = 32 * b"\0"
for i in range(1024):
if i >= len(self.__open_transactions):
transaction_data = 53 * b"\0"
else:
transaction = self.__open_transactions[i]
transaction_data = transaction.open_transactions_hash_data()
current_hash = hashlib.sha256(current_hash + transaction_data).digest()
self.__hashes.append(current_hash)
class Blockchain:
def __init__(self):
# maps block hashes to block instances
self.__block_map = {}
self.__latest_block_hash = None
self.__associated_data = {}
self.__lock = Lock()
self.open_transactions = OpenTransactions(self)
self.__load_blocks_from_disk()
def __load_blocks_from_disk(self):
last_valid = None
try:
with open("blockchain", "rb") as f:
while True:
block = f.read(293)
if len(block) < 293:
break
if associated_data_required(block[0:149]):
associated_data = f.read(5376)
if len(associated_data) < 5376:
break
else:
associated_data = None
block_obj = self.add_block(block, associated_data)
if not block_obj.validate(self):
break
self.cache_associated_data(block_obj.transaction)
last_valid = block_obj
except FileNotFoundError:
pass
if last_valid is not None:
self.set_latest_block(last_valid.own_hash, persist = False)
def set_latest_block(self, block_hash, persist = True):
new_block = self.get_block(block_hash)
assert new_block is not None
assert new_block.valid
while True:
with self.__lock:
latest_block_hash = self.__latest_block_hash
if latest_block_hash is not None:
latest_block = self.get_block(latest_block_hash)
current_difficulty_sum = latest_block.get_difficulty_info(1, self)[0]
new_difficulty_sum = new_block.get_difficulty_info(1, self)[0]
if new_difficulty_sum <= current_difficulty_sum:
return False
else:
latest_block = None
with self.__lock:
if self.__latest_block_hash != latest_block_hash:
continue
self.__latest_block_hash = block_hash
if persist:
self.__persist_block_update(latest_block, new_block)
self.open_transactions.update()
return True
def __persist_block_update(self, old_block, new_block):
if old_block is not None:
while old_block.block_number > new_block.block_number:
old_block = self.__block_map[old_block.previous_hash]
block_list = []
while new_block is not old_block:
block_list.append(new_block)
if new_block.previous_hash == 32 * b"\0":
break
new_block = self.__block_map[new_block.previous_hash]
if old_block is not None and old_block.block_number > new_block.block_number:
old_block = self.__block_map[old_block.previous_hash]
start_addr = block_list[-1].persist_address
open_mode = "wb" if start_addr == 0 else "r+b"
with open("blockchain", open_mode) as f:
f.seek(start_addr)
for block in reversed(block_list):
f.write(block.get_block_raw())
if isinstance(block.transaction, RevealTransaction):
f.write(block.transaction.get_associated_data())
f.truncate()
def add_block(self, block_raw, associated_data = None):
with self.__lock:
block = Block.from_bytes(block_raw, associated_data)
if block.own_hash not in self.__block_map:
self.__block_map[block.own_hash] = block
return self.__block_map[block.own_hash]
def get_block(self, hash):
with self.__lock:
return self.__block_map.get(hash)
def get_second_last_difficulty_sum(self):
with self.__lock:
latest_block_hash = self.__latest_block_hash
if latest_block_hash is None:
return 0
block = self.get_block(latest_block_hash)
return block.get_difficulty_info(1, self)[0]
def get_latest_block(self):
with self.__lock:
if self.__latest_block_hash is None:
return None
return self.__block_map[self.__latest_block_hash]
def cache_associated_data(self, transaction):
if not isinstance(transaction, RevealTransaction):
return
with self.__lock:
self.__associated_data[transaction.associated_proof_hash] = transaction.get_associated_data()
def get_associated_data(self, associated_proof_hash):
with self.__lock:
return self.__associated_data.get(associated_proof_hash, None)