Add peer card discovery scaffold

This commit is contained in:
Eric Wendland 2026-05-16 14:24:21 +02:00
commit 81149605bc
11 changed files with 271 additions and 15 deletions

View file

@ -1,18 +1,149 @@
use geth_types::NodeId;
use geth_types::{AgentId, NodeId, UnixMillis};
use serde::{Deserialize, Serialize};
pub const PEER_CARD_SIGNATURE_NAMESPACE: &str = "geth.peer-card.v1@geth.local";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerCard {
pub node: NodeId,
pub endpoints: Vec<String>,
pub signed_by: String,
pub node_id: NodeId,
pub agent_id: AgentId,
pub endpoints: Vec<EndpointCandidate>,
pub issued_at: UnixMillis,
pub signature: SignatureMetadata,
}
impl PeerCard {
pub fn validate_candidate(&self) -> Result<(), DiscoveryError> {
if self.endpoints.is_empty() {
return Err(DiscoveryError::MissingEndpoint);
}
if self.signature.namespace != PEER_CARD_SIGNATURE_NAMESPACE {
return Err(DiscoveryError::InvalidSignatureNamespace(
self.signature.namespace.clone(),
));
}
if self.signature.signer.is_empty() || self.signature.signature.is_empty() {
return Err(DiscoveryError::UnsignedPeerCard);
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EndpointCandidate {
pub endpoint_id: String,
pub relay_url: Option<String>,
pub source: DiscoverySource,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignatureMetadata {
pub namespace: String,
pub signer: String,
pub signature: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DiscoverySource {
Manual,
Mdns,
PeerExchange,
Imported,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscoveredPeer {
pub card: PeerCard,
pub discovered_at: UnixMillis,
pub source: DiscoverySource,
pub trust_state: CandidateTrustState,
}
impl DiscoveredPeer {
pub fn candidate(
card: PeerCard,
discovered_at: UnixMillis,
source: DiscoverySource,
) -> Result<Self, DiscoveryError> {
card.validate_candidate()?;
Ok(Self {
card,
discovered_at,
source,
trust_state: CandidateTrustState::CandidateOnly,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CandidateTrustState {
CandidateOnly,
}
pub trait DiscoveryBackend {
fn candidates(&self) -> Vec<PeerCard>;
fn candidates(&self) -> Result<Vec<DiscoveredPeer>, DiscoveryError>;
}
#[derive(Debug, thiserror::Error)]
pub enum DiscoveryError {
#[error("peer card has no endpoint candidates")]
MissingEndpoint,
#[error("peer card is missing signature metadata")]
UnsignedPeerCard,
#[error("invalid peer card signature namespace: {0}")]
InvalidSignatureNamespace(String),
}
#[must_use]
pub fn discovery_is_untrusted_note() -> &'static str {
"discovery returns candidate peers only and never grants trust or authorization"
}
#[cfg(test)]
mod tests {
use super::*;
fn signed_card() -> PeerCard {
PeerCard {
node_id: "node:laptop".into(),
agent_id: "agent:abc".into(),
endpoints: vec![EndpointCandidate {
endpoint_id: "endpoint:iroh".to_owned(),
relay_url: None,
source: DiscoverySource::Manual,
}],
issued_at: UnixMillis(1),
signature: SignatureMetadata {
namespace: PEER_CARD_SIGNATURE_NAMESPACE.to_owned(),
signer: "agent:abc".to_owned(),
signature: "sig:test".to_owned(),
},
}
}
#[test]
fn signed_peer_card_is_valid_candidate() {
signed_card()
.validate_candidate()
.expect("valid signed candidate");
}
#[test]
fn discovered_peer_is_candidate_only() {
let peer = DiscoveredPeer::candidate(signed_card(), UnixMillis(2), DiscoverySource::Mdns)
.expect("candidate");
assert_eq!(peer.trust_state, CandidateTrustState::CandidateOnly);
}
#[test]
fn unsigned_peer_card_is_not_valid_candidate() {
let mut card = signed_card();
card.signature.signature.clear();
assert!(matches!(
card.validate_candidate(),
Err(DiscoveryError::UnsignedPeerCard)
));
}
}