From 1521fb498afa85ee6d03bdb347e700da2187d85c Mon Sep 17 00:00:00 2001 From: eunhhu Date: Tue, 29 Sep 2026 16:44:34 +0100 Subject: Link a node to a receiver written from the spec alone check.py's server_links_independent is a receiver in Python (the cryptography package, no code of link/) written from the link protocol's description: the static secret, HKDF's keys, the nonces, the frames, a CHALLENGE, ACK and STORED, and ACCEPT for epoch 1. A pure-serve node links to it: every one of 200 orders is stored as the node knows it, the first packet opened in epoch 0 and the other 199 in the epoch the two made apart. None where the package is not there. --- tests/check.py | 224 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) (limited to 'tests/check.py') diff --git a/tests/check.py b/tests/check.py index 190a856..84cf0c7 100644 --- a/tests/check.py +++ b/tests/check.py @@ -1019,6 +1019,229 @@ def server_links_refused(aup, pure, work): return {'said': said[0]['error'], 'packets': stats['a']['link_packets']} +def server_links_independent(aup, pure, work): + """A receiver written from docs/design/012-link-protocol.md alone, in + Python with the cryptography package and no code of link/: a Pure node + links to it (pure://). It checks the node's address with a CHALLENGE, + opens the node's packets, takes its records in order, answers with ACK + and STORED, and accepts its epoch 1, so the node goes on in the epoch + the two made. In the end the node knows every order it saved stored, + and some of them came sealed in epoch 1. None where the cryptography + package is not there.""" + try: + from cryptography.hazmat.primitives import hashes, serialization + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey + from cryptography.hazmat.primitives.ciphers.aead import AESGCM + from cryptography.hazmat.primitives.kdf.hkdf import HKDF + except ImportError: + return None + import os + import select + import socket + import struct + import threading + import time + + def hkdf(secret, info, size): + return HKDF(algorithm=hashes.SHA256(), length=size, salt=b'pure link 0', info=info).derive(secret) + + def raw(key): + return key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + + def key_id(public): + h = 2166136261 + for byte in public: + h = (h ^ byte) * 16777619 & 0xffffffff + return h + + def packet_keys(secret, sender, receiver, sender_run, receiver_run): + out = hkdf(secret, b'pure link 0 keys' + sender + receiver + struct.pack('Q', pn))) + + def names(body, at): + a = body[at] + b = body[at + 1 + a] + return body[at + 1:at + 1 + a], body[at + 2 + a:at + 2 + a + b], at + 2 + a + b + + def varint(body, at): + value = shift = 0 + while True: + byte = body[at] + value |= (byte & 0x7f) << shift + at += 1 + if not byte & 0x80: + return value, at + shift += 7 + + module = work / 'independent-sender.pmod' + source = module.with_suffix('.aup') + source.write_text(LINK_SENDER) + compile_module(aup, source, module) + + def start(args, port): + process = subprocess.Popen([str(a) for a in args] + ['--port', str(port)], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + ready, _, _ = select.select([process.stdout], [], [], 10) + line = process.stdout.readline() if ready else b'' + assert line, f'host did not start: {process.stderr.read(2000) if process.poll() is not None else ""}' + return process, json.loads(line) + + mine = X25519PrivateKey.generate() + my_public = raw(mine) + link = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + link.bind(('127.0.0.1', 0)) + with socket.socket() as probe: + probe.bind(('127.0.0.1', 0)) + port = probe.getsockname()[1] + node, info = start([pure / 'build/pure-serve', '--module', module, '--store', work / 'independent-a', + '--key', work / 'independent.key', '--send', + f'ordered=pure://127.0.0.1:{link.getsockname()[1]}/order', '--seal', f'ordered={my_public.hex()}'], + port) + theirs = bytes.fromhex(info['public_key']) + static = mine.exchange(X25519PublicKey.from_public_bytes(theirs)) + orders = 200 + + def buy(): + import http.client + for n in range(orders): + connection = http.client.HTTPConnection('127.0.0.1', port, timeout=10) + body = json.dumps({'item': f'i{n}', 'count': 1}).encode() + connection.request('POST', '/inputs/buy', body, {'Content-Length': str(len(body)), + 'Content-Type': 'application/json'}) + assert connection.getresponse().status == 200 + connection.close() + + buyer = threading.Thread(target=buy) + buyer.start() + runs = {} # the node's runs: their epochs, answers and the numbers had + stream, heard, early = None, 0, {} + opened = {0: 0, 1: 0} + deadline, done_at = time.time() + 30, None + try: + while time.time() < deadline and (done_at is None or time.time() < done_at): + ready, _, _ = select.select([link], [], [], 0.05) + if not ready: + continue + packet, address = link.recvfrom(2048) + if not 37 <= len(packet) <= 1452 or packet[0] >> 4 != 0x7: + continue + key, run, pn = struct.unpack_from(' max(r['had']) + r['had'].add(pn) + if newest and address != r['address']: + r['address'], r['checked'], r['bytes_in'], r['bytes_out'] = address, False, 0, 0 + r['bytes_in'] += len(packet) + at, offer = 0, None + while at < len(body): + kind = body[at] + at += 1 + if kind == 0x01: # STREAM + source_name, input_name, at = names(body, at) + at += 4 + stream = (source_name, input_name) + assert input_name == b'order' and source_name == b'independent-sender.ordered', stream + elif kind == 0x02: # RECORD: small sends come whole + number, size, offset, length = struct.unpack_from(' 0 and all(r['checked'] for r in runs.values()), (opened, runs.keys()) + return {'orders': orders, 'opened_in_epoch_0': opened[0], 'opened_in_epoch_1': opened[1], + 'node_packets': stats['link_packets']} + + def server_links_moved(aup, pure, work): """A receiver's store moved to another address (H3 of docs/design/011-pure-transport.md), in namespaces of its own (unshare @@ -2266,6 +2489,7 @@ def main(): report['server_links_udp'] = server_links_udp(aup, pure, work) report['server_links_udp_lossy'] = server_links_udp_lossy(aup, pure, work) report['server_links_refused'] = server_links_refused(aup, pure, work) + report['server_links_independent'] = server_links_independent(aup, pure, work) report['server_links_moved'] = server_links_moved(aup, pure, work) report['server_webhook'] = server_webhook(aup, pure, work) report['server_sealed'] = server_sealed(aup, pure, work) -- cgit v1.2.3