From 4c4c6ce99f1bb821adb5425591ab94a0dcfe095e Mon Sep 17 00:00:00 2001 From: Evan Quiney Date: Thu, 19 Feb 2026 17:19:31 +0000 Subject: [PATCH] simplify rust ident module this is partly dead code, partly narrowing the rust-python boundary in prep for future rewrites. no testing as this is all type safe refactoring. --- rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi | 115 +------------- rust/exo_pyo3_bindings/src/ident.rs | 146 ++---------------- rust/exo_pyo3_bindings/src/lib.rs | 5 +- rust/exo_pyo3_bindings/src/networking.rs | 8 +- src/exo/main.py | 2 +- src/exo/master/tests/test_master.py | 4 +- src/exo/routing/connection_message.py | 2 +- src/exo/routing/router.py | 6 +- .../shared/tests/test_node_id_persistence.py | 2 +- 9 files changed, 39 insertions(+), 251 deletions(-) diff --git a/rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi b/rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi index fa6700ff..3d55c9e8 100644 --- a/rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi +++ b/rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi @@ -19,7 +19,7 @@ class ConnectionUpdate: Whether this is a connection or disconnection event """ @property - def peer_id(self) -> PeerId: + def peer_id(self) -> builtins.str: r""" Identity of the peer that we have connected to or disconnected from. """ @@ -40,92 +40,22 @@ class Keypair: Identity keypair of a node. """ @staticmethod - def generate_ed25519() -> Keypair: + def generate() -> Keypair: r""" Generate a new Ed25519 keypair. """ @staticmethod - def generate_ecdsa() -> Keypair: + def from_bytes(bytes: bytes) -> Keypair: r""" - Generate a new ECDSA keypair. - """ - @staticmethod - def generate_secp256k1() -> Keypair: - r""" - Generate a new Secp256k1 keypair. - """ - @staticmethod - def from_protobuf_encoding(bytes: bytes) -> Keypair: - r""" - Decode a private key from a protobuf structure and parse it as a `Keypair`. - """ - @staticmethod - def rsa_from_pkcs8(bytes: bytes) -> Keypair: - r""" - Decode an keypair from a DER-encoded secret key in PKCS#8 `PrivateKeyInfo` - format (i.e. unencrypted) as defined in [RFC5208]. - - [RFC5208]: https://tools.ietf.org/html/rfc5208#section-5 - """ - @staticmethod - def secp256k1_from_der(bytes: bytes) -> Keypair: - r""" - Decode a keypair from a DER-encoded Secp256k1 secret key in an `ECPrivateKey` - structure as defined in [RFC5915]. - - [RFC5915]: https://tools.ietf.org/html/rfc5915 - """ - @staticmethod - def ed25519_from_bytes(bytes: bytes) -> Keypair: ... - def to_protobuf_encoding(self) -> bytes: - r""" - Encode a private key as protobuf structure. - """ - def to_peer_id(self) -> PeerId: - r""" - Convert the `Keypair` into the corresponding `PeerId`. - """ - -@typing.final -class Multiaddr: - r""" - Representation of a Multiaddr. - """ - @staticmethod - def empty() -> Multiaddr: - r""" - Create a new, empty multiaddress. - """ - @staticmethod - def with_capacity(n: builtins.int) -> Multiaddr: - r""" - Create a new, empty multiaddress with the given capacity. - """ - @staticmethod - def from_bytes(bytes: bytes) -> Multiaddr: - r""" - Parse a `Multiaddr` value from its byte slice representation. - """ - @staticmethod - def from_string(string: builtins.str) -> Multiaddr: - r""" - Parse a `Multiaddr` value from its string representation. - """ - def len(self) -> builtins.int: - r""" - Return the length in bytes of this multiaddress. - """ - def is_empty(self) -> builtins.bool: - r""" - Returns true if the length of this multiaddress is 0. + Construct an Ed25519 keypair from secret key bytes """ def to_bytes(self) -> bytes: r""" - Return a copy of this [`Multiaddr`]'s byte representation. + Get the secret key bytes underlying the keypair """ - def to_string(self) -> builtins.str: + def to_node_id(self) -> builtins.str: r""" - Convert a Multiaddr to a string. + Convert the `Keypair` into the corresponding `PeerId` string, which we use as our `NodeId`. """ @typing.final @@ -180,37 +110,6 @@ class NoPeersSubscribedToTopicError(builtins.Exception): def __repr__(self) -> builtins.str: ... def __str__(self) -> builtins.str: ... -@typing.final -class PeerId: - r""" - Identifier of a peer of the network. - - The data is a `CIDv0` compatible multihash of the protobuf encoded public key of the peer - as specified in [specs/peer-ids](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md). - """ - @staticmethod - def random() -> PeerId: - r""" - Generates a random peer ID from a cryptographically secure PRNG. - - This is useful for randomly walking on a DHT, or for testing purposes. - """ - @staticmethod - def from_bytes(bytes: bytes) -> PeerId: - r""" - Parses a `PeerId` from bytes. - """ - def to_bytes(self) -> bytes: - r""" - Returns a raw bytes representation of this `PeerId`. - """ - def to_base58(self) -> builtins.str: - r""" - Returns a base-58 encoded string of this `PeerId`. - """ - def __repr__(self) -> builtins.str: ... - def __str__(self) -> builtins.str: ... - @typing.final class ConnectionUpdateType(enum.Enum): r""" diff --git a/rust/exo_pyo3_bindings/src/ident.rs b/rust/exo_pyo3_bindings/src/ident.rs index 3c27526a..55f40bc6 100644 --- a/rust/exo_pyo3_bindings/src/ident.rs +++ b/rust/exo_pyo3_bindings/src/ident.rs @@ -1,8 +1,6 @@ use crate::ext::ResultExt as _; -use libp2p::PeerId; use libp2p::identity::Keypair; -use pyo3::prelude::{PyBytesMethods as _, PyModule, PyModuleMethods as _}; -use pyo3::types::PyBytes; +use pyo3::types::{PyBytes, PyBytesMethods as _}; use pyo3::{Bound, PyResult, Python, pyclass, pymethods}; use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -18,142 +16,32 @@ pub struct PyKeypair(pub Keypair); impl PyKeypair { /// Generate a new Ed25519 keypair. #[staticmethod] - fn generate_ed25519() -> Self { + fn generate() -> Self { Self(Keypair::generate_ed25519()) } - /// Generate a new ECDSA keypair. + /// Construct an Ed25519 keypair from secret key bytes #[staticmethod] - fn generate_ecdsa() -> Self { - Self(Keypair::generate_ecdsa()) - } - - /// Generate a new Secp256k1 keypair. - #[staticmethod] - fn generate_secp256k1() -> Self { - Self(Keypair::generate_secp256k1()) - } - - /// Decode a private key from a protobuf structure and parse it as a `Keypair`. - #[staticmethod] - fn from_protobuf_encoding(bytes: Bound<'_, PyBytes>) -> PyResult { - let bytes = Vec::from(bytes.as_bytes()); - Ok(Self(Keypair::from_protobuf_encoding(&bytes).pyerr()?)) - } - - /// Decode an keypair from a DER-encoded secret key in PKCS#8 `PrivateKeyInfo` - /// format (i.e. unencrypted) as defined in [RFC5208]. - /// - /// [RFC5208]: https://tools.ietf.org/html/rfc5208#section-5 - #[staticmethod] - fn rsa_from_pkcs8(bytes: Bound<'_, PyBytes>) -> PyResult { - let mut bytes = Vec::from(bytes.as_bytes()); - Ok(Self(Keypair::rsa_from_pkcs8(&mut bytes).pyerr()?)) - } - - /// Decode a keypair from a DER-encoded Secp256k1 secret key in an `ECPrivateKey` - /// structure as defined in [RFC5915]. - /// - /// [RFC5915]: https://tools.ietf.org/html/rfc5915 - #[staticmethod] - fn secp256k1_from_der(bytes: Bound<'_, PyBytes>) -> PyResult { - let mut bytes = Vec::from(bytes.as_bytes()); - Ok(Self(Keypair::secp256k1_from_der(&mut bytes).pyerr()?)) - } - - #[staticmethod] - fn ed25519_from_bytes(bytes: Bound<'_, PyBytes>) -> PyResult { + fn from_bytes(bytes: Bound<'_, PyBytes>) -> PyResult { let mut bytes = Vec::from(bytes.as_bytes()); Ok(Self(Keypair::ed25519_from_bytes(&mut bytes).pyerr()?)) } - /// Encode a private key as protobuf structure. - fn to_protobuf_encoding<'py>(&self, py: Python<'py>) -> PyResult> { - let bytes = self.0.to_protobuf_encoding().pyerr()?; + /// Get the secret key bytes underlying the keypair + fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + let bytes = self + .0 + .clone() + .try_into_ed25519() + .pyerr()? + .secret() + .as_ref() + .to_vec(); Ok(PyBytes::new(py, &bytes)) } - /// Convert the `Keypair` into the corresponding `PeerId`. - fn to_peer_id(&self) -> PyPeerId { - PyPeerId(self.0.public().to_peer_id()) - } - - // /// Hidden constructor for pickling support. TODO: figure out how to do pickling... - // #[gen_stub(skip)] - // #[new] - // fn py_new(bytes: Bound<'_, PyBytes>) -> PyResult { - // Self::from_protobuf_encoding(bytes) - // } - // - // #[gen_stub(skip)] - // fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> { - // *self = Self::from_protobuf_encoding(state)?; - // Ok(()) - // } - // - // #[gen_stub(skip)] - // fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult> { - // self.to_protobuf_encoding(py) - // } - // - // #[gen_stub(skip)] - // pub fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<(Bound<'py, PyBytes>,)> { - // Ok((self.to_protobuf_encoding(py)?,)) - // } -} - -/// Identifier of a peer of the network. -/// -/// The data is a `CIDv0` compatible multihash of the protobuf encoded public key of the peer -/// as specified in [specs/peer-ids](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md). -#[gen_stub_pyclass] -#[pyclass(name = "PeerId", frozen)] -#[derive(Debug, Clone)] -#[repr(transparent)] -pub struct PyPeerId(pub PeerId); - -#[gen_stub_pymethods] -#[pymethods] -#[allow(clippy::needless_pass_by_value)] -impl PyPeerId { - /// Generates a random peer ID from a cryptographically secure PRNG. - /// - /// This is useful for randomly walking on a DHT, or for testing purposes. - #[staticmethod] - fn random() -> Self { - Self(PeerId::random()) - } - - /// Parses a `PeerId` from bytes. - #[staticmethod] - fn from_bytes(bytes: Bound<'_, PyBytes>) -> PyResult { - let bytes = Vec::from(bytes.as_bytes()); - Ok(Self(PeerId::from_bytes(&bytes).pyerr()?)) - } - - /// Returns a raw bytes representation of this `PeerId`. - fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> { - let bytes = self.0.to_bytes(); - PyBytes::new(py, &bytes) - } - - /// Returns a base-58 encoded string of this `PeerId`. - fn to_base58(&self) -> String { - self.0.to_base58() - } - - fn __repr__(&self) -> String { - format!("PeerId({})", self.to_base58()) - } - - fn __str__(&self) -> String { - self.to_base58() + /// Convert the `Keypair` into the corresponding `PeerId` string, which we use as our `NodeId`. + fn to_node_id(&self) -> String { + self.0.public().to_peer_id().to_base58() } } - -pub fn ident_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - - Ok(()) -} diff --git a/rust/exo_pyo3_bindings/src/lib.rs b/rust/exo_pyo3_bindings/src/lib.rs index 45825b21..8c1fb6b5 100644 --- a/rust/exo_pyo3_bindings/src/lib.rs +++ b/rust/exo_pyo3_bindings/src/lib.rs @@ -8,9 +8,10 @@ mod allow_threading; mod ident; mod networking; -use crate::ident::ident_submodule; +use crate::ident::PyKeypair; use crate::networking::networking_submodule; use pyo3::prelude::PyModule; +use pyo3::types::PyModuleMethods; use pyo3::{Bound, PyResult, pyclass, pymodule}; use pyo3_stub_gen::define_stub_info_gatherer; @@ -158,7 +159,7 @@ fn main_module(m: &Bound<'_, PyModule>) -> PyResult<()> { // TODO: for now this is all NOT a submodule, but figure out how to make the submodule system // work with maturin, where the types generate correctly, in the right folder, without // too many importing issues... - ident_submodule(m)?; + m.add_class::()?; networking_submodule(m)?; // top-level constructs diff --git a/rust/exo_pyo3_bindings/src/networking.rs b/rust/exo_pyo3_bindings/src/networking.rs index b864d876..2fb1d78e 100644 --- a/rust/exo_pyo3_bindings/src/networking.rs +++ b/rust/exo_pyo3_bindings/src/networking.rs @@ -8,7 +8,7 @@ use crate::r#const::MPSC_CHANNEL_SIZE; use crate::ext::{ByteArrayExt as _, FutureExt, PyErrExt as _}; use crate::ext::{ResultExt as _, TokioMpscReceiverExt as _, TokioMpscSenderExt as _}; -use crate::ident::{PyKeypair, PyPeerId}; +use crate::ident::PyKeypair; use crate::pyclass; use libp2p::futures::StreamExt as _; use libp2p::gossipsub; @@ -119,7 +119,7 @@ struct PyConnectionUpdate { /// Identity of the peer that we have connected to or disconnected from. #[pyo3(get)] - peer_id: PyPeerId, + peer_id: String, /// Remote connection's IPv4 address. #[pyo3(get)] @@ -251,7 +251,7 @@ async fn networking_task( // send connection event to channel (or exit if connection closed) if let Err(e) = connection_update_tx.send(PyConnectionUpdate { update_type: PyConnectionUpdateType::Connected, - peer_id: PyPeerId(peer_id), + peer_id: peer_id.to_base58(), remote_ipv4, remote_tcp_port, }).await { @@ -272,7 +272,7 @@ async fn networking_task( // send disconnection event to channel (or exit if connection closed) if let Err(e) = connection_update_tx.send(PyConnectionUpdate { update_type: PyConnectionUpdateType::Disconnected, - peer_id: PyPeerId(peer_id), + peer_id: peer_id.to_base58(), remote_ipv4, remote_tcp_port, }).await { diff --git a/src/exo/main.py b/src/exo/main.py index ec203181..27c78165 100644 --- a/src/exo/main.py +++ b/src/exo/main.py @@ -45,7 +45,7 @@ class Node: @classmethod async def create(cls, args: "Args") -> "Self": keypair = get_node_id_keypair() - node_id = NodeId(keypair.to_peer_id().to_base58()) + node_id = NodeId(keypair.to_node_id()) session_id = SessionId(master_node_id=node_id, election_clock=0) router = Router.create(keypair) await router.register_topic(topics.GLOBAL_EVENTS) diff --git a/src/exo/master/tests/test_master.py b/src/exo/master/tests/test_master.py index 9452108f..fcf71ee4 100644 --- a/src/exo/master/tests/test_master.py +++ b/src/exo/master/tests/test_master.py @@ -42,7 +42,7 @@ from exo.utils.channels import channel @pytest.mark.asyncio async def test_master(): keypair = get_node_id_keypair() - node_id = NodeId(keypair.to_peer_id().to_base58()) + node_id = NodeId(keypair.to_node_id()) session_id = SessionId(master_node_id=node_id, election_clock=0) ge_sender, global_event_receiver = channel[ForwarderEvent]() @@ -75,7 +75,7 @@ async def test_master(): async with anyio.create_task_group() as tg: tg.start_soon(master.run) - sender_node_id = NodeId(f"{keypair.to_peer_id().to_base58()}_sender") + sender_node_id = NodeId(f"{keypair.to_node_id()}_sender") # inject a NodeGatheredInfo event logger.info("inject a NodeGatheredInfo event") await local_event_sender.send( diff --git a/src/exo/routing/connection_message.py b/src/exo/routing/connection_message.py index 665483ac..0eb68f37 100644 --- a/src/exo/routing/connection_message.py +++ b/src/exo/routing/connection_message.py @@ -30,7 +30,7 @@ class ConnectionMessage(CamelCaseModel): @classmethod def from_update(cls, update: ConnectionUpdate) -> "ConnectionMessage": return cls( - node_id=NodeId(update.peer_id.to_base58()), + node_id=NodeId(update.peer_id), connection_type=ConnectionMessageType.from_update_type(update.update_type), remote_ipv4=update.remote_ipv4, remote_tcp_port=update.remote_tcp_port, diff --git a/src/exo/routing/router.py b/src/exo/routing/router.py index 309f7d0b..d71275b7 100644 --- a/src/exo/routing/router.py +++ b/src/exo/routing/router.py @@ -221,7 +221,7 @@ def get_node_id_keypair( Obtain the :class:`PeerId` by from it. """ # TODO(evan): bring back node id persistence once we figure out how to deal with duplicates - return Keypair.generate_ed25519() + return Keypair.generate() def lock_path(path: str | bytes | PathLike[str] | PathLike[bytes]) -> Path: return Path(str(path) + ".lock") @@ -235,12 +235,12 @@ def get_node_id_keypair( protobuf_encoded = f.read() try: # if decoded successfully, save & return - return Keypair.from_protobuf_encoding(protobuf_encoded) + return Keypair.from_bytes(protobuf_encoded) except ValueError as e: # on runtime error, assume corrupt file logger.warning(f"Encountered error when trying to get keypair: {e}") # if no valid credentials, create new ones and persist with open(path, "w+b") as f: keypair = Keypair.generate_ed25519() - f.write(keypair.to_protobuf_encoding()) + f.write(keypair.to_bytes()) return keypair diff --git a/src/exo/shared/tests/test_node_id_persistence.py b/src/exo/shared/tests/test_node_id_persistence.py index c067bd3c..ce9e56f5 100644 --- a/src/exo/shared/tests/test_node_id_persistence.py +++ b/src/exo/shared/tests/test_node_id_persistence.py @@ -23,7 +23,7 @@ def _get_keypair_concurrent_subprocess_task( sem.release() # wait to be told to begin simultaneous read ev.wait() - queue.put(get_node_id_keypair().to_protobuf_encoding()) + queue.put(get_node_id_keypair().to_bytes()) def _get_keypair_concurrent(num_procs: int) -> bytes: