"""Byte-level comparison: our RI serialization vs Java peer RI.""" import asyncio import hashlib import io import struct import sys sys.path.insert(0, "src") from i2p_data.router import RouterInfo, RouterAddress, _serialize_mapping, _parse_mapping from i2p_data.keys_and_cert import KeysAndCert from i2p_netdb.reseed import ReseedClient from i2p_router.identity import ( RouterKeyBundle, create_full_router_identity, ) def hexdump_ri(label, ri_bytes): """Parse and hexdump each section of a RouterInfo.""" print(f"\n=== {label} (total {len(ri_bytes)} bytes) ===") stream = io.BytesIO(ri_bytes) # Identity: 256 + 128 + cert pub_area = stream.read(256) sig_area = stream.read(128) cert_header = stream.read(3) cert_type, cert_len = struct.unpack("!BH", cert_header) cert_payload = stream.read(cert_len) if cert_len > 0 else b"" identity_end = stream.tell() print(f"Identity ({identity_end} bytes):") print(f" Pub area (256): ...{pub_area[-36:].hex()}") print(f" Sig area (128): ...{sig_area[-36:].hex()}") print(f" Cert: type={cert_type} len={cert_len} payload={cert_payload.hex()}") # Published published = struct.unpack("!Q", stream.read(8))[0] print(f"Published: {published} (0x{published:016x})") # Address count addr_count = struct.unpack("!B", stream.read(1))[0] print(f"Address count: {addr_count}") # Addresses for i in range(addr_count): addr_start = stream.tell() cost = struct.unpack("!B", stream.read(1))[0] expiration = struct.unpack("!Q", stream.read(8))[0] transport_len = struct.unpack("!B", stream.read(1))[0] transport = stream.read(transport_len).decode("utf-8") props_len = struct.unpack("!H", stream.read(2))[0] props_raw = stream.read(props_len) if props_len > 0 else b"" addr_end = stream.tell() print(f"Address[{i}] ({addr_end - addr_start} bytes):") print(f" cost={cost} exp={expiration} transport='{transport}'") print(f" props_len={props_len}") print(f" props_raw ({len(props_raw)} bytes): {props_raw.hex()}") # Parse and reserialize to compare parsed_props = _parse_mapping(props_raw) reserialized = _serialize_mapping(parsed_props) print(f" props_parsed: {parsed_props}") print(f" props_reserialized ({len(reserialized)} bytes): {reserialized.hex()}") print(f" MATCH: {props_raw == reserialized}") if props_raw != reserialized: # Find first difference for j in range(min(len(props_raw), len(reserialized))): if props_raw[j] != reserialized[j]: print(f" FIRST DIFF at byte {j}: original=0x{props_raw[j]:02x} reserialized=0x{reserialized[j]:02x}") print(f" Context: original[{max(0,j-5)}:{j+10}]={props_raw[max(0,j-5):j+10].hex()}") print(f" Context: reserial[{max(0,j-5)}:{j+10}]={reserialized[max(0,j-5):j+10].hex()}") break if len(props_raw) != len(reserialized): print(f" LENGTH DIFF: original={len(props_raw)} reserialized={len(reserialized)}") # Peer size peer_size = struct.unpack("!B", stream.read(1))[0] print(f"Peer size: {peer_size}") # Options opts_len = struct.unpack("!H", stream.read(2))[0] opts_raw = stream.read(opts_len) if opts_len > 0 else b"" print(f"Options (len={opts_len}):") print(f" opts_raw ({len(opts_raw)} bytes): {opts_raw.hex()}") parsed_opts = _parse_mapping(opts_raw) reserialized_opts = _serialize_mapping(parsed_opts) print(f" opts_parsed: {parsed_opts}") print(f" opts_reserialized ({len(reserialized_opts)} bytes): {reserialized_opts.hex()}") print(f" MATCH: {opts_raw == reserialized_opts}") if opts_raw != reserialized_opts: for j in range(min(len(opts_raw), len(reserialized_opts))): if opts_raw[j] != reserialized_opts[j]: print(f" FIRST DIFF at byte {j}: original=0x{opts_raw[j]:02x} reserialized=0x{reserialized_opts[j]:02x}") print(f" Context orig: {opts_raw[max(0,j-5):j+10].hex()}") print(f" Context rser: {reserialized_opts[max(0,j-5):j+10].hex()}") break if len(opts_raw) != len(reserialized_opts): print(f" LENGTH DIFF: original={len(opts_raw)} reserialized={len(reserialized_opts)}") # Signature sig_pos = stream.tell() sig = stream.read() print(f"Signature ({len(sig)} bytes at offset {sig_pos}): {sig.hex()}") # Verify signable bytes roundtrip signable_end = sig_pos signable_raw = ri_bytes[:signable_end] ri_obj = RouterInfo.from_bytes(ri_bytes) signable_computed = ri_obj._signable_bytes() print(f"\nSignable bytes comparison:") print(f" Raw from wire: {len(signable_raw)} bytes") print(f" Computed: {len(signable_computed)} bytes") print(f" MATCH: {signable_raw == signable_computed}") if signable_raw != signable_computed: for j in range(min(len(signable_raw), len(signable_computed))): if signable_raw[j] != signable_computed[j]: print(f" FIRST DIFF at byte {j}: wire=0x{signable_raw[j]:02x} computed=0x{signable_computed[j]:02x}") print(f" Wire context [{j-10}:{j+10}]: {signable_raw[max(0,j-10):j+10].hex()}") print(f" Computed context[{j-10}:{j+10}]: {signable_computed[max(0,j-10):j+10].hex()}") break print(f" verify(): {ri_obj.verify()}") async def main(): # Generate our RI bundle = RouterKeyBundle.generate() identity, ri = create_full_router_identity(bundle, "0.0.0.0", 9000) our_ri_bytes = ri.to_bytes() hexdump_ri("OUR ROUTERINFO", our_ri_bytes) # Get a Java peer RI print("\n\nReseeding...") client = ReseedClient(target_count=5, min_servers=1, timeout=15) ri_list = await client.reseed() for i, peer_bytes in enumerate(ri_list[:3]): try: hexdump_ri(f"JAVA PEER RI #{i+1}", peer_bytes) except Exception as e: print(f"Error parsing peer RI #{i+1}: {e}") if __name__ == "__main__": asyncio.run(main())