Miners should choose a transaction from the network based on its reward, not as part of the mining variability. That's why it's a bad idea to place the transaction as last block element. Reorder the fields to make less room for harmful mining optimization.
219 lines
8.4 KiB
Python
219 lines
8.4 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(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
|
|
|
|
@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
|
|
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,
|
|
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)
|
|
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 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 Blockchain:
|
|
def __init__(self):
|
|
# maps block hashes to block instances
|
|
self.__block_map = {}
|
|
self.__latest_block_hash = None
|
|
self.__lock = Lock()
|
|
def set_latest_block(self, block_hash):
|
|
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
|
|
with self.__lock:
|
|
if self.__latest_block_hash != latest_block_hash:
|
|
continue
|
|
self.__latest_block_hash = block_hash
|
|
return True
|
|
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]
|