Skip to content

Commit dcdfab4

Browse files
authored
feat: implementing p2p-circuit (#74)
* feat: implementing p2p-circuit * chore: add comments to tests
1 parent 69336b7 commit dcdfab4

File tree

4 files changed

+214
-19
lines changed

4 files changed

+214
-19
lines changed

multiaddr/multiaddr.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,9 +360,11 @@ def _from_string(self, addr: str) -> None:
360360
try:
361361
self._bytes += varint.encode(proto.code)
362362
buf = codec.to_bytes(proto, value)
363+
# Add length prefix for variable-sized or zero-sized codecs
363364
if codec.SIZE <= 0:
364365
self._bytes += varint.encode(len(buf))
365-
self._bytes += buf
366+
if buf: # Only append buffer if it's not empty
367+
self._bytes += buf
366368
except Exception as e:
367369
raise exceptions.StringParseError(str(e), addr) from e
368370
continue
@@ -408,10 +410,17 @@ def _from_string(self, addr: str) -> None:
408410

409411
try:
410412
self._bytes += varint.encode(proto.code)
413+
414+
# Special case: protocols with codec=None are flag protocols
415+
# (no value, no length prefix, no buffer)
416+
if proto.codec is None:
417+
continue
418+
411419
buf = codec.to_bytes(proto, value or "")
412-
if codec.SIZE <= 0:
420+
if codec.SIZE <= 0: # Add length prefix for variable-sized or zero-sized codecs
413421
self._bytes += varint.encode(len(buf))
414-
self._bytes += buf
422+
if buf: # Only append buffer if it's not empty
423+
self._bytes += buf
415424
except Exception as e:
416425
raise exceptions.StringParseError(str(e), addr) from e
417426

@@ -432,13 +441,36 @@ def _from_bytes(self, addr: bytes) -> None:
432441
def get_peer_id(self) -> Optional[str]:
433442
"""Get the peer ID from the multiaddr.
434443
444+
For circuit addresses, returns the target peer ID, not the relay peer ID.
445+
435446
Returns:
436447
The peer ID if found, None otherwise.
437448
438449
Raises:
439450
BinaryParseError: If the binary multiaddr is invalid.
440451
"""
441452
try:
442-
return self.value_for_protocol(protocol_with_name("p2p").code)
443-
except ValueError:
453+
tuples = []
454+
455+
for _, proto, codec, part in bytes_iter(self._bytes):
456+
if proto.name == "p2p":
457+
tuples.append((proto, part))
458+
459+
# If this is a p2p-circuit address, reset tuples to get target peer id
460+
# not the peer id of the relay
461+
if proto.name == "p2p-circuit":
462+
tuples = []
463+
464+
# Get the last p2p tuple (target peer ID for circuits)
465+
if tuples:
466+
last_tuple = tuples[-1]
467+
proto, part = last_tuple
468+
# Get the codec for this specific protocol
469+
codec = codec_by_name(proto.codec)
470+
# Handle both fixed-size and variable-sized codecs
471+
if codec is not None and codec.SIZE != 0:
472+
return codec.to_string(proto, part)
473+
474+
return None
475+
except Exception:
444476
return None

multiaddr/transforms.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ def string_to_bytes(string: str) -> bytes:
2020
)
2121
logger.debug(
2222
f"[DEBUG string_to_bytes] Processing: proto={proto.name}, "
23-
f"codec.SIZE={getattr(codec, 'SIZE', None)}, value={value}"
23+
f"codec.SIZE={getattr(codec, 'SIZE', None) if codec else None}, value={value}"
2424
)
2525
logger.debug(f"[DEBUG string_to_bytes] Protocol code: {proto.code}")
2626
encoded_code = varint.encode(proto.code)
2727
logger.debug(f"[DEBUG string_to_bytes] Encoded protocol code: {encoded_code}")
2828
bs.append(encoded_code)
29+
30+
# Special case: protocols with codec=None are flag protocols
31+
# (no value, no length prefix, no buffer)
2932
if codec is None:
30-
raise ValueError(f"Unknown codec for protocol {proto.name}")
33+
logger.debug(
34+
f"[DEBUG string_to_bytes] Protocol {proto.name} has no data, "
35+
"skipping value encoding"
36+
)
37+
continue
38+
3139
if value is None:
3240
raise ValueError("Value cannot be None")
3341
try:
@@ -41,12 +49,15 @@ def string_to_bytes(string: str) -> bytes:
4149
f"[DEBUG string_to_bytes] Appending: proto={proto.name}, "
4250
f"codec.SIZE={getattr(codec, 'SIZE', None)}"
4351
)
52+
# Only add length prefix for variable-sized codecs (SIZE <= 0)
4453
if codec.SIZE <= 0:
4554
bs.append(varint.encode(len(buf)))
4655
logger.debug(
4756
f"[DEBUG string_to_bytes] Appending varint length: {varint.encode(len(buf))}"
4857
)
49-
bs.append(buf)
58+
# Only append the buffer if it's not empty
59+
if buf:
60+
bs.append(buf)
5061
logger.debug(f"[DEBUG string_to_bytes] Final bs: {bs}")
5162
return b"".join(bs)
5263

@@ -103,7 +114,7 @@ def size_for_addr(codec: CodecBase, buf_io: io.BytesIO) -> int:
103114

104115

105116
def string_iter(
106-
string: str
117+
string: str,
107118
) -> Generator[tuple[Protocol, CodecBase | None, str | None], None, None]:
108119
"""Iterate over the parts of a string multiaddr.
109120
@@ -136,12 +147,10 @@ def string_iter(
136147
value = parts[i + 1]
137148
i += 1 # Skip the next part since we used it as value
138149
logger.debug(f"[DEBUG string_iter] Using next part as value: {value}")
150+
yield proto, codec, value
139151
else:
140152
logger.debug(f"[DEBUG string_iter] No value found for protocol {proto.name}")
141-
logger.debug(
142-
f"[DEBUG string_iter] Yielding: proto={proto.name}, codec={codec}, value={value}"
143-
)
144-
yield proto, codec, value
153+
yield proto, codec, None
145154
i += 1
146155

147156

ruff.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
2020
quote-style = "double"
2121
indent-style = "space"
2222
skip-magic-trailing-comma = false
23-
line-ending = "lf"
23+
line-ending = "lf"
24+
25+
[lint.per-file-ignores]
26+
"tests/test_multiaddr.py" = ["E501"]

tests/test_multiaddr.py

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,11 @@ def assert_value_for_proto(multi, proto, expected):
242242
@pytest.mark.parametrize(
243243
"addr_str,proto,expected",
244244
[
245-
("/ip4/127.0.0.1/tcp/4001/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC",
246-
"p2p",
247-
"QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"),
245+
(
246+
"/ip4/127.0.0.1/tcp/4001/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC",
247+
"p2p",
248+
"QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC",
249+
),
248250
("/ip4/127.0.0.1/tcp/4001", "tcp", "4001"),
249251
("/ip4/127.0.0.1/tcp/4001", "ip4", "127.0.0.1"),
250252
("/ip4/127.0.0.1/tcp/4001", "udp", None),
@@ -264,8 +266,7 @@ def test_get_value(addr_str, proto, expected):
264266

265267
def test_get_value_original():
266268
ma = Multiaddr(
267-
"/ip4/127.0.0.1/tcp/5555/udp/1234/"
268-
"p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
269+
"/ip4/127.0.0.1/tcp/5555/udp/1234/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
269270
)
270271

271272
assert_value_for_proto(ma, P_IP4, "127.0.0.1")
@@ -436,3 +437,153 @@ def test_sequence_behavior():
436437
list(ma.items())[len(ma)]
437438
with pytest.raises(IndexError):
438439
list(ma.values())[len(ma)]
440+
441+
442+
def test_circuit_peer_id_extraction():
443+
"""Test that get_peer_id() returns the correct peer ID for circuit addresses."""
444+
445+
# Basic circuit address - should return target peer ID
446+
ma = Multiaddr("/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")
447+
assert ma.get_peer_id() == "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
448+
449+
# Circuit with relay - should return target peer ID, not relay peer ID
450+
ma = Multiaddr(
451+
"/ip4/0.0.0.0/tcp/8080/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
452+
)
453+
assert ma.get_peer_id() == "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
454+
455+
# Circuit without target peer ID - should return None
456+
ma = Multiaddr(
457+
"/ip4/127.0.0.1/tcp/123/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit"
458+
)
459+
assert ma.get_peer_id() is None
460+
461+
# Input: bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4 (CIDv1 Base32)
462+
# Expected: QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi (CIDv0 Base58btc)
463+
ma = Multiaddr("/p2p-circuit/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4")
464+
assert ma.get_peer_id() == "QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
465+
466+
# Base58btc encoded identity multihash (no conversion needed)
467+
ma = Multiaddr("/p2p-circuit/p2p/12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p")
468+
assert ma.get_peer_id() == "12D3KooWNvSZnPi3RrhrTwEY4LuuBeB6K6facKUCJcyWG1aoDd2p"
469+
470+
471+
def test_circuit_peer_id_edge_cases():
472+
"""Test edge cases for circuit peer ID extraction."""
473+
474+
# Multiple circuits - should return the target peer ID after the last circuit
475+
# Input: bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4 (CIDv1 Base32)
476+
# Expected: QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi (CIDv0 Base58btc)
477+
ma = Multiaddr(
478+
"/ip4/1.2.3.4/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p-circuit/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4"
479+
)
480+
assert ma.get_peer_id() == "QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
481+
482+
# Circuit with multiple p2p components after it
483+
# Input: bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4 (CIDv1 Base32)
484+
# Expected: QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi (CIDv0 Base58btc)
485+
ma = Multiaddr(
486+
"/ip4/1.2.3.4/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4"
487+
)
488+
assert ma.get_peer_id() == "QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
489+
490+
# Circuit at the beginning (invalid but should handle gracefully)
491+
ma = Multiaddr("/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")
492+
assert ma.get_peer_id() == "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
493+
494+
# No p2p components at all
495+
ma = Multiaddr("/ip4/127.0.0.1/tcp/1234")
496+
assert ma.get_peer_id() is None
497+
498+
# Only relay peer ID, no target
499+
ma = Multiaddr(
500+
"/ip4/127.0.0.1/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit"
501+
)
502+
assert ma.get_peer_id() is None
503+
504+
505+
def test_circuit_address_parsing():
506+
"""Test that circuit addresses can be parsed correctly."""
507+
508+
# Basic circuit address
509+
ma = Multiaddr("/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC")
510+
assert str(ma) == "/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
511+
512+
# Circuit with relay
513+
ma = Multiaddr(
514+
"/ip4/0.0.0.0/tcp/8080/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
515+
)
516+
assert "p2p-circuit" in str(ma)
517+
assert "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC" in str(ma)
518+
519+
# Input: bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4 (CIDv1 Base32)
520+
# Expected: QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi (CIDv0 Base58btc)
521+
ma = Multiaddr(
522+
"/ip4/127.0.0.1/tcp/1234/tls/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4"
523+
)
524+
assert (
525+
str(ma)
526+
== "/ip4/127.0.0.1/tcp/1234/tls/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
527+
)
528+
529+
530+
def test_circuit_address_manipulation():
531+
"""Test circuit address manipulation (encapsulate/decapsulate)."""
532+
533+
# Input: bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4 (CIDv1 Base32)
534+
# Expected: QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi (CIDv0 Base58btc)
535+
relay = Multiaddr("/ip4/127.0.0.1/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6")
536+
circuit = Multiaddr(
537+
"/p2p-circuit/p2p/bafzbeigweq4zr4x4ky2dvv7nanbkw6egutvrrvzw6g3h2rftp7gidyhtt4"
538+
)
539+
combined = relay.encapsulate(circuit)
540+
assert (
541+
str(combined)
542+
== "/ip4/127.0.0.1/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6/p2p-circuit/p2p/QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
543+
)
544+
assert combined.get_peer_id() == "QmckZzdVd72h9QUFuJJpQqhsZqGLwjhh81qSvZ9BhB2FQi"
545+
546+
# Decapsulate circuit
547+
decapsulated = combined.decapsulate("/p2p-circuit")
548+
assert (
549+
str(decapsulated)
550+
== "/ip4/127.0.0.1/tcp/1234/p2p/QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6"
551+
)
552+
assert decapsulated.get_peer_id() == "QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6"
553+
554+
555+
def test_circuit_with_consistent_cid_format():
556+
"""Test circuit functionality using consistent CIDv0 format for easier comparison."""
557+
558+
# All peer IDs in CIDv0 Base58btc format for easy visual comparison
559+
relay_peer_id = "QmZR5a9AAXGqQF2ADqoDdGS8zvqv8n3Pag6TDDnTNMcFW6"
560+
target_peer_id = "QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
561+
562+
# Basic circuit with consistent format
563+
ma = Multiaddr(f"/p2p-circuit/p2p/{target_peer_id}")
564+
assert ma.get_peer_id() == target_peer_id
565+
566+
# Circuit with relay using consistent format
567+
ma = Multiaddr(f"/ip4/127.0.0.1/tcp/1234/p2p/{relay_peer_id}/p2p-circuit/p2p/{target_peer_id}")
568+
assert ma.get_peer_id() == target_peer_id
569+
570+
# Test string representation preserves format
571+
assert (
572+
str(ma) == f"/ip4/127.0.0.1/tcp/1234/p2p/{relay_peer_id}/p2p-circuit/p2p/{target_peer_id}"
573+
)
574+
575+
# Test encapsulate/decapsulate with consistent format
576+
relay = Multiaddr(f"/ip4/127.0.0.1/tcp/1234/p2p/{relay_peer_id}")
577+
circuit = Multiaddr(f"/p2p-circuit/p2p/{target_peer_id}")
578+
combined = relay.encapsulate(circuit)
579+
580+
assert (
581+
str(combined)
582+
== f"/ip4/127.0.0.1/tcp/1234/p2p/{relay_peer_id}/p2p-circuit/p2p/{target_peer_id}"
583+
)
584+
assert combined.get_peer_id() == target_peer_id
585+
586+
# Decapsulate should return relay address
587+
decapsulated = combined.decapsulate("/p2p-circuit")
588+
assert str(decapsulated) == f"/ip4/127.0.0.1/tcp/1234/p2p/{relay_peer_id}"
589+
assert decapsulated.get_peer_id() == relay_peer_id

0 commit comments

Comments
 (0)