Files
carrotcoin/blockchain.py

349 lines
14 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
@dataclass
class Transaction:
id: int
sender: bytes
receiver: bytes
amount: int
transaction_fee: int
signature: bytes
def from_bytes(transaction_raw):
assert len(transaction_raw) == 148
return Transaction(
id = int.from_bytes(transaction_raw[0:4], "big"),
sender = transaction_raw[4:36],
receiver = transaction_raw[36:68],
amount = int.from_bytes(transaction_raw[68:76], "big"),
transaction_fee = int.from_bytes(transaction_raw[76:84], "big"),
signature = transaction_raw[84:148],
)
def is_valid(self):
sender_pubkey = Ed25519PublicKey.from_public_bytes(self.sender)
msg = 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 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 sorting_id(self):
return (-self.transaction_fee, self.sender, self.id)
def __eq__(self, other):
return (self.id, self.sender, self.receiver, self.amount, self.transaction_fee) == \
(other.id, other.sender, other.receiver, other.amount, other.transaction_fee)
@dataclass
class Block:
nonce: int
timestamp: int
previous_hash: bytes
message: bytes
difficulty_sum: int
miner_pubkey: bytes
transaction: Transaction
own_hash: bytes
balances: dict
# (sender_pubkey, id) tuples
used_transaction_ids: set
block_number: int
valid: bool
def from_bytes(block_raw):
assert len(block_raw) == 292
transaction_raw = block_raw[0:148]
if transaction_raw == 148 * b"\0":
transaction = None
else:
transaction = Transaction.from_bytes(transaction_raw)
return Block(
transaction = transaction,
message = block_raw[148:180],
miner_pubkey = block_raw[180:212],
previous_hash = block_raw[212:244],
timestamp = int.from_bytes(block_raw[244:252], "big"),
difficulty_sum = int.from_bytes(block_raw[252:284], "big"),
nonce = int.from_bytes(block_raw[284:292], "big"),
own_hash = hashlib.sha256(block_raw).digest(),
balances = None,
used_transaction_ids = None,
block_number = None,
valid = False,
)
def validate(self, blockchain):
if self.transaction is not None:
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 self.transaction is not None and not self.transaction.is_valid_after_block(prev_block):
return False
else:
prev_block = None
if self.transaction is not None:
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, B_1_timestamp = 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_balances(prev_block)
self.calculate_used_transaction_ids(prev_block)
self.calculate_block_number(prev_block)
return self.valid
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 t is not None:
balances[self.miner_pubkey] += t.transaction_fee
balances[t.sender] -= (t.amount + t.transaction_fee)
balances[t.receiver] += t.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 t is not None:
used_transaction_ids.add((t.sender, 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 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):
if self.transaction is None:
transaction = 148 * b"\0"
else:
transaction = self.transaction.get_transaction_raw()
return transaction + \
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) < 1000:
return True
return transaction.sorting_id() < self.__open_transactions[-1].sorting_id()
def __cleanup(self):
# sort everything
self.__open_transactions.sort(key = Transaction.sorting_id)
# 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()
def is_valid(transaction):
sender_tuple = (transaction.sender, transaction.id)
if sender_tuple in used_transaction_ids:
return False
balance = balances.get(transaction.sender) or 0
if transaction.amount + transaction.transaction_fee > balance:
return False
used_transaction_ids.add(sender_tuple)
balances[transaction.sender] = balance - transaction.amount - transaction.transaction_fee
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 = 44 * b"\0"
else:
transaction = self.__open_transactions[i]
transaction_data = transaction.transaction_fee.to_bytes(8, "big") + \
transaction.sender + \
transaction.id.to_bytes(4, "big")
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.__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(292)
if len(block) < 292:
break
block_obj = self.add_block(block)
if not block_obj.validate(self):
break
last_valid = block_obj
except FileNotFoundError:
pass
if last_valid is not None:
self.set_latest_block(last_valid.own_hash, persist = False)
def set_latest_block(self, block_hash, persist = True):
new_block = self.get_block(block_hash)
assert new_block is not None
assert new_block.valid
while True:
with self.__lock:
latest_block_hash = self.__latest_block_hash
if latest_block_hash is not None:
latest_block = self.get_block(latest_block_hash)
current_difficulty_sum = latest_block.get_difficulty_info(1, self)[0]
new_difficulty_sum = new_block.get_difficulty_info(1, self)[0]
if new_difficulty_sum <= current_difficulty_sum:
return False
else:
latest_block = None
with self.__lock:
if self.__latest_block_hash != latest_block_hash:
continue
self.__latest_block_hash = block_hash
if persist:
self.__persist_block_update(latest_block, new_block)
self.open_transactions.update()
return True
def __persist_block_update(self, old_block, new_block):
if old_block is not None:
while old_block.block_number > new_block.block_number:
old_block = self.__block_map[old_block.previous_hash]
block_list = []
while new_block is not old_block:
block_list.append(new_block)
if new_block.previous_hash == 32 * b"\0":
break
new_block = self.__block_map[new_block.previous_hash]
if old_block is not None and old_block.block_number > new_block.block_number:
old_block = self.__block_map[old_block.previous_hash]
start_block_number = block_list[-1].block_number
start_addr = start_block_number * 292
open_mode = "wb" if start_addr == 0 else "r+b"
with open("blockchain", open_mode) as f:
f.seek(start_addr)
for block in reversed(block_list):
f.write(block.get_block_raw())
def add_block(self, block_raw):
with self.__lock:
block = Block.from_bytes(block_raw)
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]