Skip to content

Commit 0f29ab3

Browse files
committed
Implement p2p key codec as per the (new) spec
1 parent 60ada21 commit 0f29ab3

File tree

7 files changed

+211
-35
lines changed

7 files changed

+211
-35
lines changed

multiaddr/codecs/cid.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import base58
2+
import cid
3+
4+
from . import LENGTH_PREFIXED_VAR_SIZE
5+
6+
7+
SIZE = LENGTH_PREFIXED_VAR_SIZE
8+
IS_PATH = False
9+
10+
11+
# Spec: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
12+
CIDv0_PREFIX_TO_LENGTH = {
13+
# base58btc prefixes for valid lengths 1 – 42 with the identity “hash” function
14+
'12': [5, 12, 19, 23, 30, 41, 52, 56],
15+
'13': [9, 16, 34, 45],
16+
'14': [27, 38, 49, 60],
17+
'15': [3, 6, 20],
18+
'16': [3, 6, 13, 20, 31, 42, 53],
19+
'17': [3, 13, 42],
20+
'18': [3],
21+
'19': [3, 24, 57],
22+
'1A': [24, 35, 46],
23+
'1B': [35],
24+
'1D': [17],
25+
'1E': [10, 17],
26+
'1F': [10],
27+
'1G': [10, 28, 50],
28+
'1H': [28, 39],
29+
'1P': [21],
30+
'1Q': [21],
31+
'1R': [21, 54],
32+
'1S': [54],
33+
'1T': [7, 32, 43],
34+
'1U': [7, 32, 43],
35+
'1V': [7],
36+
'1W': [7, 14],
37+
'1X': [7, 14],
38+
'1Y': [7, 14],
39+
'1Z': [7, 14],
40+
'1f': [4],
41+
'1g': [4, 58],
42+
'1h': [4, 25, 58],
43+
'1i': [4, 25],
44+
'1j': [4, 25],
45+
'1k': [4, 25, 47],
46+
'1m': [4, 36, 47],
47+
'1n': [4, 36],
48+
'1o': [4, 36],
49+
'1p': [4],
50+
'1q': [4],
51+
'1r': [4],
52+
'1s': [4],
53+
'1t': [4],
54+
'1u': [4],
55+
'1v': [4],
56+
'1w': [4],
57+
'1x': [4],
58+
'1y': [4],
59+
'1z': [4, 18],
60+
61+
# base58btc prefix for length 42 with the sha256 hash function
62+
'Qm': [46],
63+
}
64+
65+
PROTO_NAME_TO_CIDv1_CODEC = {
66+
# The “p2p” multiaddr protocol requires all keys to use the “libp2p-key” multicodec
67+
"p2p": "libp2p-key",
68+
}
69+
70+
71+
def to_bytes(proto, string):
72+
expected_codec = PROTO_NAME_TO_CIDv1_CODEC.get(proto.name)
73+
74+
if len(string) in CIDv0_PREFIX_TO_LENGTH.get(string[0:2], ()): # CIDv0
75+
# Upgrade the wire (binary) representation of any received CIDv0 string
76+
# to CIDv1 if we can determine which multicodec value to use
77+
if expected_codec:
78+
return cid.make_cid(1, expected_codec, base58.b58decode(string)).buffer
79+
80+
return base58.b58decode(string)
81+
else: # CIDv1+
82+
parsed = cid.from_string(string)
83+
84+
# Ensure CID has correct codec for protocol
85+
if expected_codec and parsed.codec != expected_codec:
86+
raise ValueError("“{0}” multiaddr CIDs must use the “{1}” multicodec"
87+
.format(proto.name, expected_codec))
88+
89+
return parsed.buffer
90+
91+
92+
def _is_binary_cidv0_multihash(buf):
93+
if buf.startswith(b"\x12\x20") and len(buf) == 34: # SHA2-256
94+
return True
95+
96+
if (buf[0] == 0x00 and buf[1] in range(43)) and len(buf) == (buf[1] + 2): # Identity hash
97+
return True
98+
99+
return False
100+
101+
102+
def to_string(proto, buf):
103+
expected_codec = PROTO_NAME_TO_CIDv1_CODEC.get(proto.name)
104+
105+
if _is_binary_cidv0_multihash(buf): # CIDv0
106+
if not expected_codec:
107+
# Simply encode as base58btc as there is nothing better to do
108+
return base58.b58encode(buf).decode('ascii')
109+
110+
# “Implementations SHOULD display peer IDs using the first (raw
111+
# base58btc encoded multihash) format until the second format is
112+
# widely supported.”
113+
#
114+
# In the future the following line should instead convert the multihash
115+
# to CIDv1 and with the `expected_codec` and wrap it in base32:
116+
# return cid.make_cid(1, expected_codec, buf).encode("base32").decode("ascii")
117+
return base58.b58encode(buf).decode("ascii")
118+
else: # CIDv1+
119+
parsed = cid.from_bytes(buf)
120+
121+
# Ensure CID has correct codec for protocol
122+
if expected_codec and parsed.codec != expected_codec:
123+
raise ValueError("“{0}” multiaddr CIDs must use the “{1}” multicodec"
124+
.format(proto.name, expected_codec))
125+
126+
# “Implementations SHOULD display peer IDs using the first (raw
127+
# base58btc encoded multihash) format until the second format is
128+
# widely supported.”
129+
if expected_codec and _is_binary_cidv0_multihash(parsed.multihash):
130+
return base58.b58encode(parsed.multihash).decode("ascii")
131+
132+
return parsed.encode("base32").decode("ascii")

multiaddr/codecs/p2p.py

Lines changed: 0 additions & 18 deletions
This file was deleted.

multiaddr/protocols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def __repr__(self):
132132
Protocol(P_SCTP, 'sctp', 'uint16be'),
133133
Protocol(P_UDT, 'udt', None),
134134
Protocol(P_UTP, 'utp', None),
135-
Protocol(P_P2P, 'p2p', 'p2p'),
135+
Protocol(P_P2P, 'p2p', 'cid'),
136136
Protocol(P_ONION, 'onion', 'onion'),
137137
Protocol(P_ONION3, 'onion3', 'onion3'),
138138
Protocol(P_QUIC, 'quic', None),

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ varint
22
base58
33
netaddr
44
idna
5+
py-cid
6+
py-multicodec >= 0.2.0

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
'varint',
5555
'base58',
5656
'netaddr',
57+
'py-cid',
58+
'py-multicodec >= 0.2.0',
5759
],
5860
test_suite='tests',
5961
tests_require=[

tests/test_multiaddr.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ def test_invalid(addr_str):
100100
"/unix/Überrschung!/大柱",
101101
"/unix/stdio",
102102
"/ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f",
103-
"/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234/unix/stdio",
103+
"/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC"
104+
"/tcp/1234/unix/stdio",
104105
"/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct",
105106
"/dns/example.com",
106107
"/dns4/موقع.وزارة-الاتصالات.مصر"]) # nopep8
@@ -174,8 +175,9 @@ def test_invalid_protocols_with_string(proto_string):
174175
("/ip4/0.0.0.0", 0, ("/ip4/0.0.0.0",)),
175176
("/ip6/::1", 1, ("/ip6/::1",)),
176177
("/onion/timaq4ygg2iegci7:80/http", 0, ("/onion/timaq4ygg2iegci7:80/http",)),
177-
("/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234", 1,
178-
("/ip4/127.0.0.1", "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234",)),
178+
("/ip4/127.0.0.1/p2p/bafzbeigvf25ytwc3akrijfecaotc74udrhcxzh2cx3we5qqnw5vgrei4bm/tcp/1234", 1,
179+
("/ip4/127.0.0.1",
180+
"/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234")),
179181
("/ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f", -1,
180182
("/ip4/1.2.3.4", "/tcp/80", "/unix/a/b/c/d/e/f"))])
181183
def test_split(proto_string, maxsplit, expected):
@@ -188,7 +190,8 @@ def test_split(proto_string, maxsplit, expected):
188190
((b"\x04\x00\x00\x00\x00",), "/ip4/0.0.0.0"),
189191
(("/ip6/::1",), "/ip6/::1"),
190192
(("/onion/timaq4ygg2iegci7:80/http",), "/onion/timaq4ygg2iegci7:80/http"),
191-
((b"\x04\x7F\x00\x00\x01", "/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234",),
193+
((b"\x04\x7F\x00\x00\x01",
194+
"/p2p/bafzbeigvf25ytwc3akrijfecaotc74udrhcxzh2cx3we5qqnw5vgrei4bm/tcp/1234",),
192195
"/ip4/127.0.0.1/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234"),
193196
(("/ip4/1.2.3.4", "/tcp/80", "/unix/a/b/c/d/e/f"), "/ip4/1.2.3.4/tcp/80/unix/a/b/c/d/e/f")])
194197
def test_join(proto_parts, expected):
@@ -223,7 +226,7 @@ def assert_value_for_proto(multi, proto, expected):
223226
def test_get_value():
224227
ma = Multiaddr(
225228
"/ip4/127.0.0.1/utp/tcp/5555/udp/1234/utp/"
226-
"p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP")
229+
"p2p/bafzbeigalb34xlqdtvyklzqa5ibmn6pssqsdskc4ty2e4jxy2kamquh22y")
227230

228231
assert_value_for_proto(ma, P_IP4, "127.0.0.1")
229232
assert_value_for_proto(ma, P_UTP, None)
@@ -273,7 +276,7 @@ def test_get_value():
273276
def test_views():
274277
ma = Multiaddr(
275278
"/ip4/127.0.0.1/utp/tcp/5555/udp/1234/utp/"
276-
"p2p/QmbHVEEepCi7rn7VL7Exxpd2Ci9NNB6ifvqwhsrbRMgQFP")
279+
"p2p/bafzbeigalb34xlqdtvyklzqa5ibmn6pssqsdskc4ty2e4jxy2kamquh22y")
277280

278281
for idx, (proto1, proto2, item, value) in enumerate(zip(ma, ma.keys(), ma.items(), ma.values())): # noqa: E501
279282
assert (proto1, value) == (proto2, value) == item

tests/test_transforms.py

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
from multiaddr.protocols import _names_to_protocols
1818
from multiaddr.protocols import Protocol
1919

20-
# These test values were generated by running them
21-
# through the go implementation of multiaddr.
22-
# https://github.com/multiformats/go-multiaddr
23-
ADDR_BYTES_MAP_STR_TEST_DATA = [
20+
# These test values were generated by running them through the go implementation
21+
# of multiaddr (https://github.com/multiformats/go-multiaddr)
22+
#
23+
# All values are bijective.
24+
ADDR_BYTES_STR_TEST_DATA = [
2425
(_names_to_protocols['ip4'], b'\x0a\x0b\x0c\x0d', '10.11.12.13'),
2526
(_names_to_protocols['ip6'],
2627
b'\x1a\xa1\x2b\xb2\x3c\xc3\x4d\xd4\x5e\xe5\x6f\xf6\x7a\xb7\x8a\xc8',
@@ -30,22 +31,38 @@
3031
b'\x9a\x18\x08\x73\x06\x36\x90\x43\x09\x1f\x04\xd2',
3132
'timaq4ygg2iegci7:1234'),
3233
(_names_to_protocols['p2p'],
33-
b'\x12\x20\xd5\x2e\xbb\x89\xd8\x5b\x02\xa2\x84\x94\x82\x03\xa6\x2f\xf2'
34-
b'\x83\x89\xc5\x7c\x9f\x42\xbe\xec\x4e\xc2\x0d\xb7\x6a\x68\x91\x1c\x0b',
34+
b'\x01\x72\x12\x20\xd5\x2e\xbb\x89\xd8\x5b\x02\xa2\x84\x94\x82\x03\xa6\x2f'
35+
b'\xf2\x83\x89\xc5\x7c\x9f\x42\xbe\xec\x4e\xc2\x0d\xb7\x6a\x68\x91\x1c\x0b',
3536
'QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC'),
3637

3738
# Additional test data
3839
(_names_to_protocols['dns4'],
3940
b'\xd9\x85\xd9\x88\xd9\x82\xd8\xb9.\xd9\x88\xd8\xb2\xd8\xa7\xd8\xb1\xd8\xa9'
4041
b'-\xd8\xa7\xd9\x84\xd8\xa7\xd8\xaa\xd8\xb5\xd8\xa7\xd9\x84\xd8\xa7\xd8\xaa'
4142
b'.\xd9\x85\xd8\xb5\xd8\xb1',
42-
# Explicitly mark this as unicode to force the text to be LTR in editors
43+
# Explicitly mark this as unicode, as the “u” forces the text to be displayed LTR in editors
4344
u'موقع.وزارة-الاتصالات.مصر'),
4445
(_names_to_protocols['dns4'],
4546
b'fu\xc3\x9fball.example',
4647
'fußball.example'), # This will fail if IDNA-2003/NamePrep is used
4748
]
4849

50+
ADDR_BYTES_FROM_STR_TEST_DATA = [
51+
# New CIDv1 string to new CIDv1 binary format mapping (non-bijective)
52+
(_names_to_protocols['p2p'],
53+
b'\x01\x72\x12\x20\xd5\x2e\xbb\x89\xd8\x5b\x02\xa2\x84\x94\x82\x03\xa6\x2f'
54+
b'\xf2\x83\x89\xc5\x7c\x9f\x42\xbe\xec\x4e\xc2\x0d\xb7\x6a\x68\x91\x1c\x0b',
55+
'bafzbeigvf25ytwc3akrijfecaotc74udrhcxzh2cx3we5qqnw5vgrei4bm'),
56+
]
57+
58+
ADDR_BYTES_TO_STR_TEST_DATA = [
59+
# Old CIDv0 binary to old CIDv0 string format mapping (non-bijective)
60+
(_names_to_protocols['p2p'],
61+
b'\x12\x20\xd5\x2e\xbb\x89\xd8\x5b\x02\xa2\x84\x94\x82\x03\xa6\x2f\xf2'
62+
b'\x83\x89\xc5\x7c\x9f\x42\xbe\xec\x4e\xc2\x0d\xb7\x6a\x68\x91\x1c\x0b',
63+
'QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC'),
64+
]
65+
4966
BYTES_MAP_STR_TEST_DATA = [
5067
("/ip4/127.0.0.1/udp/1234", b'\x04\x7f\x00\x00\x01\x91\x02\x04\xd2'),
5168
("/ip4/127.0.0.1/tcp/4321", b'\x04\x7f\x00\x00\x01\x06\x10\xe1'),
@@ -57,7 +74,7 @@
5774
@pytest.mark.parametrize("codec_name, buf, expected", [
5875
(None, b'\x01\x02\x03', (0, 0)),
5976
('ip4', b'\x01\x02\x03', (4, 0)),
60-
('p2p', b'\x40\x50\x60\x51', (64, 1)),
77+
('cid', b'\x40\x50\x60\x51', (64, 1)),
6178
])
6279
def test_size_for_addr(codec_name, buf, expected):
6380
buf_io = io.BytesIO(buf)
@@ -76,13 +93,14 @@ def test_bytes_iter(buf, expected):
7693
assert list((proto, val) for _, proto, _, val in bytes_iter(buf)) == expected
7794

7895

79-
@pytest.mark.parametrize("proto, buf, expected", ADDR_BYTES_MAP_STR_TEST_DATA)
96+
@pytest.mark.parametrize("proto, buf, expected",
97+
ADDR_BYTES_STR_TEST_DATA + ADDR_BYTES_TO_STR_TEST_DATA)
8098
def test_codec_to_string(proto, buf, expected):
8199
assert codec_by_name(proto.codec).to_string(proto, buf) == expected
82100

83101

84102
@pytest.mark.parametrize("proto, expected, string",
85-
ADDR_BYTES_MAP_STR_TEST_DATA)
103+
ADDR_BYTES_STR_TEST_DATA + ADDR_BYTES_FROM_STR_TEST_DATA)
86104
def test_codec_to_bytes(proto, string, expected):
87105
assert codec_by_name(proto.codec).to_bytes(proto, string) == expected
88106

@@ -149,6 +167,8 @@ def test_bytes_to_string_value_error(protocol_extension, bytes):
149167
(_names_to_protocols['onion'], 'timaq4ygg2iegci7:0'),
150168
(_names_to_protocols['onion'], 'timaq4ygg2iegci7:71234'),
151169
(_names_to_protocols['p2p'], '15230d52ebb89d85b02a284948203a'),
170+
(_names_to_protocols['p2p'], # CID type != "libp2p-key":
171+
'bafyaajaiaejcbrrv5vds2whn3c464rsb5r2vpxeanneinzlijenlac77cju2pptf'),
152172
(_names_to_protocols['ip6zone'], ""),
153173
])
154174
def test_codec_to_bytes_value_error(proto, address):
@@ -160,10 +180,45 @@ def test_codec_to_bytes_value_error(proto, address):
160180

161181
@pytest.mark.parametrize("proto, buf", [
162182
(_names_to_protocols['tcp'], b'\xff\xff\xff\xff'),
183+
(_names_to_protocols['p2p'], # CID type != "libp2p-key":
184+
b"\x01\x70\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41"
185+
b"\xec\x75\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65"),
163186
(_names_to_protocols['ip6zone'], b""),
164187
])
165188
def test_codec_to_string_value_error(proto, buf):
166189
# Codecs themselves may raise any exception type – it will then be converted
167190
# to `BinaryParseError` by a higher level
168191
with pytest.raises(Exception):
169192
codec_by_name(proto.codec).to_string(proto, buf)
193+
194+
195+
@pytest.mark.parametrize("proto, string, expected", [
196+
(_names_to_protocols["p2p"], # This one gets autoconverted to CIDv1
197+
"12D3KooWPA6ax6t3jqTyGq73Zm1RmwppYqxaXzrtarfcTWGp5Wzx",
198+
b"\x01\x72\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41"
199+
b"\xec\x75\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65"),
200+
(_names_to_protocols["ip6"], # Others do not
201+
"12D3KooWPA6ax6t3jqTyGq73Zm1RmwppYqxaXzrtarfcTWGp5Wzx",
202+
b"\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41\xec\x75"
203+
b"\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65"),
204+
])
205+
def test_cid_autoconvert_to_bytes(proto, string, expected):
206+
assert codec_by_name("cid").to_bytes(proto, string) == expected
207+
208+
209+
@pytest.mark.parametrize("proto, buf, expected", [
210+
(_names_to_protocols["p2p"], # This one gets autoconverted to CIDv0
211+
b"\x01\x72\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41"
212+
b"\xec\x75\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65",
213+
"12D3KooWPA6ax6t3jqTyGq73Zm1RmwppYqxaXzrtarfcTWGp5Wzx"),
214+
(_names_to_protocols["ip6"], # Others do not
215+
b"\x01\x72\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41"
216+
b"\xec\x75\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65",
217+
"bafzaajaiaejcbrrv5vds2whn3c464rsb5r2vpxeanneinzlijenlac77cju2pptf"),
218+
(_names_to_protocols["ip6"], # (Needed to put identity conversion test somewhere)
219+
b"\x00\x24\x08\x01\x12\x20\xc6\x35\xed\x47\x2d\x58\xed\xd8\xb9\xee\x46\x41\xec\x75"
220+
b"\x57\xdc\x80\x6b\x48\x86\xe5\x68\x49\x1a\xb0\x0b\xff\x12\x69\xa7\xbe\x65",
221+
"12D3KooWPA6ax6t3jqTyGq73Zm1RmwppYqxaXzrtarfcTWGp5Wzx"),
222+
])
223+
def test_cid_autoconvert_to_string(proto, buf, expected):
224+
assert codec_by_name("cid").to_string(proto, buf) == expected

0 commit comments

Comments
 (0)