Skip to content

Commit f24859b

Browse files
committed
Init py-peer-record module
1 parent 94d695c commit f24859b

File tree

9 files changed

+477
-0
lines changed

9 files changed

+477
-0
lines changed

libp2p/records/__init__.py

Whitespace-only changes.

libp2p/records/pb/record.proto

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
syntax = "proto3";
2+
package record.pb;
3+
4+
option go_package = "github.com/libp2p/go-libp2p-record/pb";
5+
6+
// Record represents a dht record that contains a value
7+
// for a key value pair
8+
message Record {
9+
// The key that references this record
10+
bytes key = 1;
11+
12+
// The actual value this record is storing
13+
bytes value = 2;
14+
15+
// Note: These fields were removed from the Record message
16+
// hash of the authors public key
17+
// optional string author = 3;
18+
// A PKI signature for the key+value+author
19+
// optional bytes signature = 4;
20+
21+
// Time the record was received, set by receiver
22+
string timeReceived = 5;
23+
}

libp2p/records/pb/record_pb2.py

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libp2p/records/pb/record_pb2.pyi

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
@generated by mypy-protobuf. Do not edit manually!
3+
isort:skip_file
4+
"""
5+
import builtins
6+
import google.protobuf.descriptor
7+
import google.protobuf.message
8+
import sys
9+
10+
if sys.version_info >= (3, 8):
11+
import typing as typing_extensions
12+
else:
13+
import typing_extensions
14+
15+
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
16+
17+
class Record(google.protobuf.message.Message):
18+
"""Record represents a dht record that contains a value
19+
for a key value pair
20+
"""
21+
22+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
23+
24+
KEY_FIELD_NUMBER: builtins.int
25+
VALUE_FIELD_NUMBER: builtins.int
26+
TIMERECEIVED_FIELD_NUMBER: builtins.int
27+
key: builtins.bytes
28+
"""The key that references this record"""
29+
value: builtins.bytes
30+
"""The actual value this record is storing"""
31+
timeReceived: builtins.str
32+
"""Note: These fields were removed from the Record message
33+
hash of the authors public key
34+
optional string author = 3;
35+
A PKI signature for the key+value+author
36+
optional bytes signature = 4;
37+
38+
Time the record was received, set by receiver
39+
"""
40+
def __init__(
41+
self,
42+
*,
43+
key: builtins.bytes = ...,
44+
value: builtins.bytes = ...,
45+
timeReceived: builtins.str = ...,
46+
) -> None: ...
47+
def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "timeReceived", b"timeReceived", "value", b"value"]) -> None: ...
48+
49+
global___Record = Record

libp2p/records/pubkey.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import multihash
2+
3+
from libp2p.crypto.ed25519 import Ed25519PublicKey
4+
from libp2p.crypto.keys import PublicKey
5+
from libp2p.crypto.pb import crypto_pb2
6+
from libp2p.crypto.rsa import RSAPublicKey
7+
from libp2p.crypto.secp256k1 import Secp256k1PublicKey
8+
from libp2p.peer.id import ID
9+
from libp2p.records.utils import InvalidRecordType, split_key
10+
from libp2p.records.validator import Validator
11+
12+
13+
class PublicKeyValidator(Validator):
14+
"""
15+
Validator for public key records.
16+
"""
17+
18+
def validate(self, key: str, value: bytes) -> None:
19+
"""
20+
Validate a public key record.
21+
22+
Args:
23+
key (str): The key associated with the record.
24+
value (bytes): The value of the record, expected to be a public key.
25+
26+
Raises:
27+
InvalidRecordType: If the namespace is not 'pk', the key
28+
is not a valid multihash,
29+
the public key cannot be unmarshaled, the peer ID cannot be derived, or
30+
the public key does not match the storage key.
31+
32+
"""
33+
ns, key = split_key(key)
34+
if ns != "pk":
35+
raise InvalidRecordType("namespace not 'pk'")
36+
37+
keyhash = bytes.fromhex(key)
38+
try:
39+
_ = multihash.decode(keyhash)
40+
except Exception:
41+
raise InvalidRecordType("key did not contain valid multihash")
42+
43+
try:
44+
pubkey = unmarshal_public_key(value)
45+
except Exception:
46+
raise InvalidRecordType("Unable to unmarshal public key")
47+
48+
try:
49+
peer_id = ID.from_pubkey(pubkey)
50+
except Exception:
51+
raise InvalidRecordType("Could not derive peer ID from public key")
52+
53+
if peer_id.to_bytes() != keyhash:
54+
raise InvalidRecordType("public key does not match storage key")
55+
56+
def select(self, key: str, values: list[bytes]) -> int:
57+
"""
58+
Select a value from a list of public key records.
59+
60+
Args:
61+
key (str): The key associated with the records.
62+
values (list[bytes]): A list of public key values.
63+
64+
Returns:
65+
int: Always returns 0 as all public keys are treated identically.
66+
67+
"""
68+
return 0 # All public keys are treated identical
69+
70+
71+
def unmarshal_public_key(data: bytes) -> PublicKey:
72+
"""
73+
Deserialize a public key from its serialized byte representation.
74+
This function takes a byte sequence representing a serialized public key
75+
and reconstructs the corresponding `PublicKey` object based on its type.
76+
77+
Args:
78+
data (bytes): The serialized byte representation of the public key.
79+
80+
Returns:
81+
PublicKey: The deserialized public key object.
82+
83+
Raises:
84+
ValueError: If the key type is unsupported or unrecognized.
85+
Supported Key Types:
86+
- RSA
87+
- Ed25519
88+
- Secp256k1
89+
90+
"""
91+
proto_key = crypto_pb2.PublicKey.FromString(data)
92+
key_type = proto_key.key_type
93+
key_data = proto_key.data
94+
95+
if key_type == crypto_pb2.KeyType.RSA:
96+
return RSAPublicKey.from_bytes(key_data)
97+
elif key_type == crypto_pb2.KeyType.Ed25519:
98+
return Ed25519PublicKey.from_bytes(key_data)
99+
elif key_type == crypto_pb2.KeyType.Secp256k1:
100+
return Secp256k1PublicKey.from_bytes(key_data)
101+
else:
102+
raise ValueError(f"Unsupported key type: {key_type}")

libp2p/records/record.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from libp2p.records.pb import record_pb2
2+
3+
4+
def make_put_record(key: str, value: bytes) -> record_pb2.Record:
5+
"""
6+
Create a new Record object with the specified key and value.
7+
8+
Args:
9+
key (str): The key for the record, which will be encoded as bytes.
10+
value (bytes): The value to associate with the key in the record.
11+
12+
Returns:
13+
record_pb2.Record: A Record object containing the provided key and value.
14+
15+
"""
16+
record = record_pb2.Record()
17+
record.key = key.encode()
18+
record.value = value
19+
return record

libp2p/records/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
class InvalidRecordType(Exception):
2+
pass
3+
4+
5+
def split_key(key: str) -> tuple[str, str]:
6+
"""
7+
Split a record key into its type and the rest. The key must start with
8+
'/' and contain another '/' to separate the type. Raises `InvalidRecordType`
9+
if the key is invalid.
10+
11+
Args:
12+
key (str): The record key to split.
13+
14+
Returns:
15+
tuple[str, str]: The key type and the rest.
16+
17+
"""
18+
if not key or key[0] != "/":
19+
raise InvalidRecordType("Invalid record keytype")
20+
21+
key = key[1:]
22+
23+
i = key.find("/")
24+
if i <= 0:
25+
raise InvalidRecordType("Invalid record keytype")
26+
27+
return key[:i], key[i + 1 :]

libp2p/records/validator.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from libp2p.records.utils import InvalidRecordType, split_key
2+
3+
4+
class ErrBetterRecord(Exception):
5+
def __init__(self, key: str, value: bytes):
6+
self.key = key
7+
self.value = value
8+
super().__init__(f'Found better value for "{key}')
9+
10+
11+
class Validator:
12+
"""Base class for all validators"""
13+
14+
def validate(self, key: str, value: bytes) -> None:
15+
raise NotImplementedError
16+
17+
def select(self, key: str, values: list[bytes]) -> int:
18+
raise NotImplementedError
19+
20+
21+
class NamespacedValidator:
22+
"""
23+
Manages a collection of validators, each associated with a specific namespace.
24+
"""
25+
26+
def __init__(self, validators: dict[str, Validator]):
27+
self._validators = validators
28+
29+
def validator_by_key(self, key: str) -> Validator | None:
30+
"""
31+
Retrieve the validator responsible for the given key's namespace.
32+
33+
Args:
34+
key (str): A namespaced key in the form "namespace/value".
35+
36+
Returns:
37+
Optional[Validator]: The matching validator, or None if not found.
38+
39+
"""
40+
try:
41+
ns, _ = split_key(key)
42+
except InvalidRecordType:
43+
return None
44+
return self._validators.get(ns)
45+
46+
def validate(self, key: str, value: bytes) -> None:
47+
"""
48+
Validate a key-value pair using the appropriate namespaced validator.
49+
50+
Args:
51+
key (str): The namespaced key (e.g., "pk/Qm...").
52+
value (bytes): The value to be validated.
53+
54+
Raises:
55+
InvalidRecordType: If no matching validator is found.
56+
Exception: Propagates any exception raised by the sub-validator.
57+
58+
"""
59+
validator = self.validator_by_key(key)
60+
if validator is None:
61+
raise InvalidRecordType("Invalid record keytype")
62+
return validator.validate(key, value)
63+
64+
def select(self, key: str, values: list[bytes]) -> int:
65+
"""
66+
Choose the best value from a list using the namespaced validator.
67+
68+
Args:
69+
key (str): The namespaced key used to find the validator.
70+
values (List[bytes]): List of candidate values to choose from.
71+
72+
Returns:
73+
int: Index of the selected best value in the input list.
74+
75+
Raises:
76+
ValueError: If the values list is empty.
77+
InvalidRecordType: If no matching validator is found.
78+
79+
"""
80+
if not values:
81+
raise ValueError("Can't select from empty value list")
82+
validator = self.validator_by_key(key)
83+
if validator is None:
84+
raise InvalidRecordType("Invalid record keytype")
85+
return validator.select(key, values)

0 commit comments

Comments
 (0)