Implement exchange of open transactions

This commit is contained in:
2024-03-20 22:23:26 +01:00
parent 0218e34787
commit c6394d2ca1
5 changed files with 233 additions and 8 deletions

View File

@@ -50,6 +50,11 @@ class Transaction:
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 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 @dataclass
class Block: class Block:
@@ -178,6 +183,82 @@ class Block:
self.difficulty_sum.to_bytes(32, "big") + \ self.difficulty_sum.to_bytes(32, "big") + \
self.nonce.to_bytes(8, "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, transation.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_pubkey + \
transaction.id.to_bytes(4, "big")
current_hash = hashlib.sha256(current_hash + transaction_data).digest()
self.__hashes.append(current_hash)
class Blockchain: class Blockchain:
def __init__(self): def __init__(self):
# maps block hashes to block instances # maps block hashes to block instances
@@ -185,6 +266,7 @@ class Blockchain:
self.__latest_block_hash = None self.__latest_block_hash = None
self.__lock = Lock() self.__lock = Lock()
self.__load_blocks_from_disk() self.__load_blocks_from_disk()
self.open_transactions = OpenTransactions(self)
def __load_blocks_from_disk(self): def __load_blocks_from_disk(self):
last_valid = None last_valid = None
try: try:
@@ -222,6 +304,7 @@ class Blockchain:
self.__latest_block_hash = block_hash self.__latest_block_hash = block_hash
if persist: if persist:
self.__persist_block_update(latest_block, new_block) self.__persist_block_update(latest_block, new_block)
self.open_transactions.update()
return True return True
def __persist_block_update(self, old_block, new_block): def __persist_block_update(self, old_block, new_block):
if old_block is not None: if old_block is not None:

View File

@@ -178,3 +178,24 @@ The node tells the miner "timestamp", "previous hash", "difficulty sum" and "tra
The miner fills "nonce", "message" and "miner pubkey" on its own. 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)
| content | size (bytes) |
|---|---|
| protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 |
| type = 9 (BE) | 1 |
| transaction | 148 |
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).
A response is always sent back in this case, even if the transaction cannot be applied to the blockchain right now.
### Payment request received (Node -> Client)
| content | size (bytes) |
|---|---|
| protocol version = 0 (BE) | 2 |
| capable version = 0 (BE) | 2 |
| type = 10 (BE) | 1 |

44
list-open-transactions.py Executable file
View File

@@ -0,0 +1,44 @@
#! /usr/bin/env python3
import base64, socket, sys
def get_transaction(s, i):
request = b"\0\0\0\0\x05" + i.to_bytes(2, "big")
for _ in range(10):
s.sendto(request, ("::1", 62039))
try:
while True:
msg, sender = s.recvfrom(4096)
if sender[0:2] == ("::1", 62039) and len(msg) == 155 and msg[0:5] == b"\0\0\0\0\x06" and msg[5:7] == i.to_bytes(2, "big"):
return msg[7:155]
except TimeoutError:
pass
return None
def format_addr(raw_addr):
return base64.b64encode(raw_addr).decode()
def format_amount(raw_amount):
int_amount = int.from_bytes(raw_amount, "big")
coins = int_amount // 100
cents = int_amount % 100
return f"{coins},{cents:02} cc"
def main():
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.settimeout(1)
for i in range(1024):
transaction = get_transaction(s, i)
if transaction is None:
print("- no response from local node -", file=sys.stderr)
exit(1)
if transaction == 148 * b"\0":
return
sender = format_addr(transaction[4:36])
receiver = format_addr(transaction[36:68])
amount = format_amount(transaction[68:76])
fee = format_amount(transaction[76:84])
print(f"{sender} {receiver} {amount:>13} {fee:>10}")
if __name__ == '__main__':
main()

93
node.py
View File

@@ -82,19 +82,12 @@ def wait_until(target_time):
if duration > 0: if duration > 0:
time.sleep(duration) time.sleep(duration)
def empty_transaction_list_hash():
current_hash = 32 * b"\0"
for _ in range(1024):
entry = current_hash + 44 * b"\0"
current_hash = hashlib.sha256(entry).digest()
return current_hash
def send_heartbeat(node, peer, b): def send_heartbeat(node, peer, b):
protocol_version = 2 * b"\0" protocol_version = 2 * b"\0"
capable_version = 2 * b"\0" capable_version = 2 * b"\0"
msg_type = b"\0" msg_type = b"\0"
difficulty_sum = b.get_second_last_difficulty_sum().to_bytes(32, "big") difficulty_sum = b.get_second_last_difficulty_sum().to_bytes(32, "big")
hash_value = empty_transaction_list_hash() hash_value = b.open_transactions.get_hash(1023)
if peer.partner is None: if peer.partner is None:
partner_ipv6 = 16 * b"\0" partner_ipv6 = 16 * b"\0"
@@ -223,6 +216,35 @@ def transfer_block(addr, node, receive_observer, b):
except NoReponseException: except NoReponseException:
pass pass
def compare_open_transactions(addr, node, receive_observer, b):
try:
cursor = 511
offset = 256
subscription = receive_observer.listen((addr[0:2], "transaction hash"))
while True:
request = b"\0\0\0\0\x03" + cursor.to_bytes(2, "big")
def list_hash_condition(response):
return response["position"] == cursor
list_hash = request_retry(node, addr, request, subscription, list_hash_condition)
if list_hash["hash"] == b.open_transactions.get_hash(cursor):
cursor += max(offset, 1)
else:
cursor -= offset
if offset == 0:
break
offset //= 2
subscription = receive_observer.listen((addr[0:2], "transaction"))
request = b"\0\0\0\0\x05" + cursor.to_bytes(2, "big")
def transaction_condition(response):
return response["position"] == cursor
remote_transaction = request_retry(node, addr, request, subscription, transaction_condition)
parsed_transaction = blockchain.Transaction.from_bytes(remote_transaction["transaction"])
if not parsed_transaction.is_valid():
return
b.open_transactions.add(parsed_transaction)
except NoReponseException:
pass
def receiver(node, b): def receiver(node, b):
receive_observer = observer.Observer() receive_observer = observer.Observer()
while True: while True:
@@ -254,10 +276,14 @@ def receiver(node, b):
partner_port = int.from_bytes(partner_info[16:18], "big") partner_port = int.from_bytes(partner_info[16:18], "big")
node.add_partner((partner_ip, partner_port)) node.add_partner((partner_ip, partner_port))
contained_difficulty_sum = int.from_bytes(msg[5:37]) contained_difficulty_sum = int.from_bytes(msg[5:37])
contained_transaction_hash = msg[37:69]
my_difficulty_sum = b.get_second_last_difficulty_sum() my_difficulty_sum = b.get_second_last_difficulty_sum()
if contained_difficulty_sum > my_difficulty_sum: if contained_difficulty_sum > my_difficulty_sum:
log("beginning a block transfer ...") log("beginning a block transfer ...")
threading.Thread(target = transfer_block, args=(addr, node, receive_observer, b)).start() threading.Thread(target = transfer_block, args=(addr, node, receive_observer, b)).start()
elif contained_transaction_hash != b.open_transactions.get_hash(1023):
log("comparing open transactions ...")
threading.Thread(target = compare_open_transactions, args=(addr, node, receive_observer, b)).start()
elif msg_type == 1: elif msg_type == 1:
# block request # block request
if msg_len != 37: if msg_len != 37:
@@ -276,6 +302,7 @@ def receiver(node, b):
# block transfer # block transfer
if msg_len != 297: if msg_len != 297:
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 297 bytes)")
continue
block_raw = msg[5:297] block_raw = msg[5:297]
new_block = b.add_block(block_raw) new_block = b.add_block(block_raw)
block_hash = new_block.own_hash block_hash = new_block.own_hash
@@ -283,6 +310,56 @@ def receiver(node, b):
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)
elif msg_type == 3:
# open transaction list hash request
if msg_len != 7:
log(f"Got an open transaction list hash request of wrong length ({msg_len} bytes from {sender}, but expected 7 bytes)")
continue
list_position = int.from_bytes(msg[5:7], "big")
if list_position >= 1024:
log(f"Got an open transaction list hash request with invalid position (position {list_position} from {sender}, but must be below 1024)")
continue
list_hash = b.open_transactions.get_hash(list_position)
response = b"\0\0\0\0\x04" + list_position.to_bytes(2, "big") + list_hash
node.node_socket.sendto(response, addr)
elif msg_type == 4:
# open transaction list hash response
if msg_len != 39:
log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 39 bytes)")
continue
event_obj = {
"position": int.from_bytes(msg[5:7], "big"),
"hash": msg[7:39],
}
identifier = (addr[0:2], "transaction hash")
receive_observer.publish(identifier, event_obj)
elif msg_type == 5:
# open transaction request
if msg_len != 7:
log(f"Got an open transaction request of wrong length ({msg_len} bytes from {sender}, but expected 7 bytes)")
continue
list_position = int.from_bytes(msg[5:7], "big")
if list_position >= 1024:
log(f"Got an open transaction request with invalid position (position {list_position} from {sender}, but must be below 1024)")
continue
transaction = b.open_transactions.get_transaction(list_position)
if transaction is None:
transaction_raw = 148 * b"\0"
else:
transaction_raw = transaction.get_transaction_raw()
response = b"\0\0\0\0\x06" + list_position.to_bytes(2, "big") + transaction_raw
node.node_socket.sendto(response, addr)
elif msg_type == 6:
# open transaction response
if msg_len != 155:
log(f"Got an open transaction list hash response of wrong length ({msg_len} bytes from {sender}, but expected 155 bytes)")
continue
event_obj = {
"position": int.from_bytes(msg[5:7], "big"),
"transaction": msg[7:155],
}
identifier = (addr[0:2], "transaction")
receive_observer.publish(identifier, event_obj)
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})")

0
open_transactions.py Normal file
View File