mirror of
https://github.com/varun-r-mallya/py-libp2p.git
synced 2025-12-31 20:36:24 +00:00
252 lines
7.6 KiB
Python
252 lines
7.6 KiB
Python
from collections.abc import Sequence
|
|
import threading
|
|
import time
|
|
from typing import Any
|
|
|
|
from multiaddr import Multiaddr
|
|
|
|
from libp2p.abc import IPeerRecord
|
|
from libp2p.peer.id import ID
|
|
import libp2p.peer.pb.peer_record_pb2 as pb
|
|
from libp2p.peer.peerinfo import PeerInfo
|
|
|
|
PEER_RECORD_ENVELOPE_DOMAIN = "libp2p-peer-record"
|
|
PEER_RECORD_ENVELOPE_PAYLOAD_TYPE = b"\x03\x01"
|
|
|
|
_last_timestamp_lock = threading.Lock()
|
|
_last_timestamp: int = 0
|
|
|
|
|
|
class PeerRecord(IPeerRecord):
|
|
"""
|
|
A record that contains metatdata about a peer in the libp2p network.
|
|
|
|
This includes:
|
|
- `peer_id`: The peer's globally unique indentifier.
|
|
- `addrs`: A list of the peer's publicly reachable multiaddrs.
|
|
- `seq`: A strictly monotonically increasing timestamp used
|
|
to order records over time.
|
|
|
|
PeerRecords are designed to be signed and transmitted in libp2p routing Envelopes.
|
|
"""
|
|
|
|
peer_id: ID
|
|
addrs: list[Multiaddr]
|
|
seq: int
|
|
|
|
def __init__(
|
|
self,
|
|
peer_id: ID | None = None,
|
|
addrs: list[Multiaddr] | None = None,
|
|
seq: int | None = None,
|
|
) -> None:
|
|
"""
|
|
Initialize a new PeerRecord.
|
|
If `seq` is not provided, a timestamp-based strictly increasing sequence
|
|
number will be generated.
|
|
|
|
:param peer_id: ID of the peer this record refers to.
|
|
:param addrs: Public multiaddrs of the peer.
|
|
:param seq: Monotonic sequence number.
|
|
|
|
"""
|
|
if peer_id is not None:
|
|
self.peer_id = peer_id
|
|
self.addrs = addrs or []
|
|
if seq is not None:
|
|
self.seq = seq
|
|
else:
|
|
self.seq = timestamp_seq()
|
|
|
|
def __repr__(self) -> str:
|
|
return (
|
|
f"PeerRecord(\n"
|
|
f" peer_id={self.peer_id},\n"
|
|
f" multiaddrs={[str(m) for m in self.addrs]},\n"
|
|
f" seq={self.seq}\n"
|
|
f")"
|
|
)
|
|
|
|
def domain(self) -> str:
|
|
"""
|
|
Return the domain string associated with this PeerRecord.
|
|
|
|
Used during record signing and envelope validation to identify the record type.
|
|
"""
|
|
return PEER_RECORD_ENVELOPE_DOMAIN
|
|
|
|
def codec(self) -> bytes:
|
|
"""
|
|
Return the codec identifier for PeerRecords.
|
|
|
|
This binary perfix helps distinguish PeerRecords in serialized envelopes.
|
|
"""
|
|
return PEER_RECORD_ENVELOPE_PAYLOAD_TYPE
|
|
|
|
def to_protobuf(self) -> pb.PeerRecord:
|
|
"""
|
|
Convert the current PeerRecord into a ProtoBuf PeerRecord message.
|
|
|
|
:raises ValueError: if peer_id serialization fails.
|
|
:return: A ProtoBuf-encoded PeerRecord message object.
|
|
"""
|
|
try:
|
|
id_bytes = self.peer_id.to_bytes()
|
|
except Exception as e:
|
|
raise ValueError(f"failed to marshal peer_id: {e}")
|
|
|
|
msg = pb.PeerRecord()
|
|
msg.peer_id = id_bytes
|
|
msg.seq = self.seq
|
|
msg.addresses.extend(addrs_to_protobuf(self.addrs))
|
|
return msg
|
|
|
|
def marshal_record(self) -> bytes:
|
|
"""
|
|
Serialize a PeerRecord into raw bytes suitable for embedding in an Envelope.
|
|
|
|
This is typically called during the process of signing or sealing the record.
|
|
:raises ValueError: if serialization to protobuf fails.
|
|
:return: Serialized PeerRecord bytes.
|
|
"""
|
|
try:
|
|
msg = self.to_protobuf()
|
|
return msg.SerializeToString()
|
|
except Exception as e:
|
|
raise ValueError(f"failed to marshal PeerRecord: {e}")
|
|
|
|
def equal(self, other: Any) -> bool:
|
|
"""
|
|
Check if this PeerRecord is identical to another.
|
|
|
|
Two PeerRecords are considered equal if:
|
|
- Their peer IDs match.
|
|
- Their sequence numbers are identical.
|
|
- Their address lists are identical and in the same order.
|
|
|
|
:param other: Another PeerRecord instance.
|
|
:return: True if all fields mathch, False otherwise.
|
|
"""
|
|
if isinstance(other, PeerRecord):
|
|
if self.peer_id == other.peer_id:
|
|
if self.seq == other.seq:
|
|
if len(self.addrs) == len(other.addrs):
|
|
for a1, a2 in zip(self.addrs, other.addrs):
|
|
if a1 == a2:
|
|
continue
|
|
else:
|
|
return False
|
|
return True
|
|
return False
|
|
|
|
|
|
def unmarshal_record(data: bytes) -> PeerRecord:
|
|
"""
|
|
Deserialize a PeerRecord from its serialized byte representation.
|
|
|
|
Typically used when receiveing a PeerRecord inside a signed routing Envelope.
|
|
|
|
:param data: Serialized protobuf-encoded bytes.
|
|
:raises ValueError: if parsing or conversion fails.
|
|
:reurn: A valid PeerRecord instance.
|
|
"""
|
|
if data is None:
|
|
raise ValueError("cannot unmarshal PeerRecord from None")
|
|
|
|
msg = pb.PeerRecord()
|
|
try:
|
|
msg.ParseFromString(data)
|
|
except Exception as e:
|
|
raise ValueError(f"Failed to parse PeerRecord protobuf: {e}")
|
|
|
|
try:
|
|
record = peer_record_from_protobuf(msg)
|
|
except Exception as e:
|
|
raise ValueError(f"Failed to convert protobuf to PeerRecord: {e}")
|
|
|
|
return record
|
|
|
|
|
|
def timestamp_seq() -> int:
|
|
"""
|
|
Generate a strictly increasing timestamp-based sequence number.
|
|
|
|
Ensures that even if multiple PeerRecords are generated in the same nanosecond,
|
|
their `seq` values will still be strictly increasing by using a lock to track the
|
|
last value.
|
|
|
|
:return: A strictly increasing integer timestamp.
|
|
"""
|
|
global _last_timestamp
|
|
now = int(time.time_ns())
|
|
with _last_timestamp_lock:
|
|
if now <= _last_timestamp:
|
|
now = _last_timestamp + 1
|
|
_last_timestamp = now
|
|
return now
|
|
|
|
|
|
def peer_record_from_peer_info(info: PeerInfo) -> PeerRecord:
|
|
"""
|
|
Create a PeerRecord from a PeerInfo object.
|
|
|
|
This automatically assigns a timestamp-based sequence number to the record.
|
|
:param info: A PeerInfo instance (contains peer_id and addrs).
|
|
:return: A PeerRecord instance.
|
|
"""
|
|
record = PeerRecord()
|
|
record.peer_id = info.peer_id
|
|
record.addrs = info.addrs
|
|
return record
|
|
|
|
|
|
def peer_record_from_protobuf(msg: pb.PeerRecord) -> PeerRecord:
|
|
"""
|
|
Convert a protobuf PeerRecord message into a PeerRecord object.
|
|
|
|
:param msg: Protobuf PeerRecord message.
|
|
:raises ValueError: if the peer_id cannot be parsed.
|
|
:return: A deserialized PeerRecord instance.
|
|
"""
|
|
try:
|
|
peer_id = ID(msg.peer_id)
|
|
except Exception as e:
|
|
raise ValueError(f"Failed to unmarshal peer_id: {e}")
|
|
|
|
addrs = addrs_from_protobuf(msg.addresses)
|
|
seq = msg.seq
|
|
|
|
return PeerRecord(peer_id, addrs, seq)
|
|
|
|
|
|
def addrs_from_protobuf(addrs: Sequence[pb.PeerRecord.AddressInfo]) -> list[Multiaddr]:
|
|
"""
|
|
Convert a list of protobuf address records to Multiaddr objects.
|
|
|
|
:param addrs: A list of protobuf PeerRecord.AddressInfo messages.
|
|
:return: A list of decoded Multiaddr instances (invalid ones are skipped).
|
|
"""
|
|
out = []
|
|
for addr_info in addrs:
|
|
try:
|
|
addr = Multiaddr(addr_info.multiaddr)
|
|
out.append(addr)
|
|
except Exception:
|
|
continue
|
|
return out
|
|
|
|
|
|
def addrs_to_protobuf(addrs: list[Multiaddr]) -> list[pb.PeerRecord.AddressInfo]:
|
|
"""
|
|
Convert a list of Multiaddr objects into their protobuf representation.
|
|
|
|
:param addrs: A list of Multiaddr instances.
|
|
:return: A list of PeerRecord.AddressInfo protobuf messages.
|
|
"""
|
|
out = []
|
|
for addr in addrs:
|
|
addr_info = pb.PeerRecord.AddressInfo()
|
|
addr_info.multiaddr = addr.to_bytes()
|
|
out.append(addr_info)
|
|
return out
|