A Python port of the Invisible Internet Project (I2P)
1"""Byte-level comparison: our RI serialization vs Java peer RI."""
2import asyncio
3import hashlib
4import io
5import struct
6import sys
7
8sys.path.insert(0, "src")
9
10from i2p_data.router import RouterInfo, RouterAddress, _serialize_mapping, _parse_mapping
11from i2p_data.keys_and_cert import KeysAndCert
12from i2p_netdb.reseed import ReseedClient
13from i2p_router.identity import (
14 RouterKeyBundle,
15 create_full_router_identity,
16)
17
18
19def hexdump_ri(label, ri_bytes):
20 """Parse and hexdump each section of a RouterInfo."""
21 print(f"\n=== {label} (total {len(ri_bytes)} bytes) ===")
22
23 stream = io.BytesIO(ri_bytes)
24
25 # Identity: 256 + 128 + cert
26 pub_area = stream.read(256)
27 sig_area = stream.read(128)
28 cert_header = stream.read(3)
29 cert_type, cert_len = struct.unpack("!BH", cert_header)
30 cert_payload = stream.read(cert_len) if cert_len > 0 else b""
31 identity_end = stream.tell()
32
33 print(f"Identity ({identity_end} bytes):")
34 print(f" Pub area (256): ...{pub_area[-36:].hex()}")
35 print(f" Sig area (128): ...{sig_area[-36:].hex()}")
36 print(f" Cert: type={cert_type} len={cert_len} payload={cert_payload.hex()}")
37
38 # Published
39 published = struct.unpack("!Q", stream.read(8))[0]
40 print(f"Published: {published} (0x{published:016x})")
41
42 # Address count
43 addr_count = struct.unpack("!B", stream.read(1))[0]
44 print(f"Address count: {addr_count}")
45
46 # Addresses
47 for i in range(addr_count):
48 addr_start = stream.tell()
49 cost = struct.unpack("!B", stream.read(1))[0]
50 expiration = struct.unpack("!Q", stream.read(8))[0]
51 transport_len = struct.unpack("!B", stream.read(1))[0]
52 transport = stream.read(transport_len).decode("utf-8")
53 props_len = struct.unpack("!H", stream.read(2))[0]
54 props_raw = stream.read(props_len) if props_len > 0 else b""
55 addr_end = stream.tell()
56 print(f"Address[{i}] ({addr_end - addr_start} bytes):")
57 print(f" cost={cost} exp={expiration} transport='{transport}'")
58 print(f" props_len={props_len}")
59 print(f" props_raw ({len(props_raw)} bytes): {props_raw.hex()}")
60 # Parse and reserialize to compare
61 parsed_props = _parse_mapping(props_raw)
62 reserialized = _serialize_mapping(parsed_props)
63 print(f" props_parsed: {parsed_props}")
64 print(f" props_reserialized ({len(reserialized)} bytes): {reserialized.hex()}")
65 print(f" MATCH: {props_raw == reserialized}")
66 if props_raw != reserialized:
67 # Find first difference
68 for j in range(min(len(props_raw), len(reserialized))):
69 if props_raw[j] != reserialized[j]:
70 print(f" FIRST DIFF at byte {j}: original=0x{props_raw[j]:02x} reserialized=0x{reserialized[j]:02x}")
71 print(f" Context: original[{max(0,j-5)}:{j+10}]={props_raw[max(0,j-5):j+10].hex()}")
72 print(f" Context: reserial[{max(0,j-5)}:{j+10}]={reserialized[max(0,j-5):j+10].hex()}")
73 break
74 if len(props_raw) != len(reserialized):
75 print(f" LENGTH DIFF: original={len(props_raw)} reserialized={len(reserialized)}")
76
77 # Peer size
78 peer_size = struct.unpack("!B", stream.read(1))[0]
79 print(f"Peer size: {peer_size}")
80
81 # Options
82 opts_len = struct.unpack("!H", stream.read(2))[0]
83 opts_raw = stream.read(opts_len) if opts_len > 0 else b""
84 print(f"Options (len={opts_len}):")
85 print(f" opts_raw ({len(opts_raw)} bytes): {opts_raw.hex()}")
86 parsed_opts = _parse_mapping(opts_raw)
87 reserialized_opts = _serialize_mapping(parsed_opts)
88 print(f" opts_parsed: {parsed_opts}")
89 print(f" opts_reserialized ({len(reserialized_opts)} bytes): {reserialized_opts.hex()}")
90 print(f" MATCH: {opts_raw == reserialized_opts}")
91 if opts_raw != reserialized_opts:
92 for j in range(min(len(opts_raw), len(reserialized_opts))):
93 if opts_raw[j] != reserialized_opts[j]:
94 print(f" FIRST DIFF at byte {j}: original=0x{opts_raw[j]:02x} reserialized=0x{reserialized_opts[j]:02x}")
95 print(f" Context orig: {opts_raw[max(0,j-5):j+10].hex()}")
96 print(f" Context rser: {reserialized_opts[max(0,j-5):j+10].hex()}")
97 break
98 if len(opts_raw) != len(reserialized_opts):
99 print(f" LENGTH DIFF: original={len(opts_raw)} reserialized={len(reserialized_opts)}")
100
101 # Signature
102 sig_pos = stream.tell()
103 sig = stream.read()
104 print(f"Signature ({len(sig)} bytes at offset {sig_pos}): {sig.hex()}")
105
106 # Verify signable bytes roundtrip
107 signable_end = sig_pos
108 signable_raw = ri_bytes[:signable_end]
109 ri_obj = RouterInfo.from_bytes(ri_bytes)
110 signable_computed = ri_obj._signable_bytes()
111 print(f"\nSignable bytes comparison:")
112 print(f" Raw from wire: {len(signable_raw)} bytes")
113 print(f" Computed: {len(signable_computed)} bytes")
114 print(f" MATCH: {signable_raw == signable_computed}")
115 if signable_raw != signable_computed:
116 for j in range(min(len(signable_raw), len(signable_computed))):
117 if signable_raw[j] != signable_computed[j]:
118 print(f" FIRST DIFF at byte {j}: wire=0x{signable_raw[j]:02x} computed=0x{signable_computed[j]:02x}")
119 print(f" Wire context [{j-10}:{j+10}]: {signable_raw[max(0,j-10):j+10].hex()}")
120 print(f" Computed context[{j-10}:{j+10}]: {signable_computed[max(0,j-10):j+10].hex()}")
121 break
122
123 print(f" verify(): {ri_obj.verify()}")
124
125
126async def main():
127 # Generate our RI
128 bundle = RouterKeyBundle.generate()
129 identity, ri = create_full_router_identity(bundle, "0.0.0.0", 9000)
130 our_ri_bytes = ri.to_bytes()
131
132 hexdump_ri("OUR ROUTERINFO", our_ri_bytes)
133
134 # Get a Java peer RI
135 print("\n\nReseeding...")
136 client = ReseedClient(target_count=5, min_servers=1, timeout=15)
137 ri_list = await client.reseed()
138
139 for i, peer_bytes in enumerate(ri_list[:3]):
140 try:
141 hexdump_ri(f"JAVA PEER RI #{i+1}", peer_bytes)
142 except Exception as e:
143 print(f"Error parsing peer RI #{i+1}: {e}")
144
145
146if __name__ == "__main__":
147 asyncio.run(main())