Implement exchange of open transactions
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
44
list-open-transactions.py
Executable 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
93
node.py
@@ -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
0
open_transactions.py
Normal file
Reference in New Issue
Block a user