← back to Exo
simplify rust ident module
4c4c6ce99f1bb821adb5425591ab94a0dcfe095e · 2026-02-19 17:19:31 +0000 · Evan Quiney
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.
Files touched
M rust/exo_pyo3_bindings/exo_pyo3_bindings.pyiM rust/exo_pyo3_bindings/src/ident.rsM rust/exo_pyo3_bindings/src/lib.rsM rust/exo_pyo3_bindings/src/networking.rsM src/exo/main.pyM src/exo/master/tests/test_master.pyM src/exo/routing/connection_message.pyM src/exo/routing/router.pyM src/exo/shared/tests/test_node_id_persistence.py
Diff
commit 4c4c6ce99f1bb821adb5425591ab94a0dcfe095e
Author: Evan Quiney <evanev7@gmail.com>
Date: Thu Feb 19 17:19:31 2026 +0000
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 +-
src/exo/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<Self> {
- 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<Self> {
- 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<Self> {
- 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<Self> {
+ fn from_bytes(bytes: Bound<'_, PyBytes>) -> PyResult<Self> {
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<Bound<'py, PyBytes>> {
- 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<Bound<'py, PyBytes>> {
+ 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> {
- // 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<Bound<'py, PyBytes>> {
- // 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<Self> {
- 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())
+ /// 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()
}
-
- fn __str__(&self) -> String {
- self.to_base58()
- }
-}
-
-pub fn ident_submodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
- m.add_class::<PyKeypair>()?;
- m.add_class::<PyPeerId>()?;
-
- 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::<PyKeypair>()?;
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:
← 42e1e732 bench: restore --danger-delete-downloads planning phase (#15
·
back to Exo
·
remove prefillprogress event (#1550) ed001f24 →