Skip to main content

calimero_network_primitives/
specialized_node_invite.rs

1//! Specialized Node Invitation Protocol Types
2//!
3//! This module defines the request-response protocol types for specialized node invitation.
4//! The protocol allows specialized nodes (e.g., read-only TEE nodes) to receive context
5//! invitations after verification.
6
7use std::io;
8
9use async_trait::async_trait;
10use borsh::{BorshDeserialize, BorshSerialize};
11use calimero_primitives::identity::PublicKey;
12use futures_util::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
13use libp2p::request_response::Codec;
14use libp2p::StreamProtocol;
15
16/// Protocol identifier for specialized node invitation request-response
17pub const CALIMERO_SPECIALIZED_NODE_INVITE_PROTOCOL: StreamProtocol =
18    StreamProtocol::new("/calimero/specialized-node-invite/1.0.0");
19
20/// Maximum size of a specialized node invite message (1MB should be sufficient for attestation + invitation)
21pub const MAX_SPECIALIZED_NODE_INVITE_MESSAGE_SIZE: u64 = 1024 * 1024;
22
23/// Type of specialized node being invited
24#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
25pub enum SpecializedNodeType {
26    /// Read-only node - receives state updates but cannot execute transactions
27    ReadOnly,
28}
29
30/// Verification request sent by specialized node to inviting node
31///
32/// After receiving a discovery message via pubsub, the specialized node sends this
33/// request containing its verification data and public key.
34///
35/// Note: context_id is NOT included - the requesting node tracks it internally
36/// using the nonce as the lookup key.
37#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
38pub enum VerificationRequest {
39    /// TEE attestation verification
40    TeeAttestation {
41        /// Nonce from the discovery message (binds attestation to request)
42        nonce: [u8; 32],
43        /// TDX/TEE attestation quote bytes
44        quote_bytes: Vec<u8>,
45        /// Specialized node's identity public key for invitation
46        public_key: PublicKey,
47    },
48    // Future variants:
49    // HardwareToken { ... },
50    // TrustedCertificate { ... },
51}
52
53impl VerificationRequest {
54    /// Get the nonce from the verification request
55    #[must_use]
56    pub fn nonce(&self) -> &[u8; 32] {
57        match self {
58            Self::TeeAttestation { nonce, .. } => nonce,
59        }
60    }
61
62    /// Get the public key from the verification request
63    #[must_use]
64    pub fn public_key(&self) -> &PublicKey {
65        match self {
66            Self::TeeAttestation { public_key, .. } => public_key,
67        }
68    }
69}
70
71/// Response sent by inviting node containing the invitation (or error)
72///
73/// After verifying the specialized node, the inviting node creates an invitation
74/// for the node's public key and sends it back.
75#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
76pub struct SpecializedNodeInvitationResponse {
77    /// The nonce from the original request (for confirmation broadcast)
78    pub nonce: [u8; 32],
79    /// Serialized ContextInvitationPayload (if verification succeeded)
80    /// We use bytes here to avoid circular dependency with context-config crate
81    pub invitation_bytes: Option<Vec<u8>>,
82    /// Error message if verification failed
83    pub error: Option<String>,
84}
85
86impl SpecializedNodeInvitationResponse {
87    /// Create a successful response with an invitation
88    #[must_use]
89    pub fn success(nonce: [u8; 32], invitation_bytes: Vec<u8>) -> Self {
90        Self {
91            nonce,
92            invitation_bytes: Some(invitation_bytes),
93            error: None,
94        }
95    }
96
97    /// Create an error response
98    #[must_use]
99    pub fn error(nonce: [u8; 32], message: impl Into<String>) -> Self {
100        Self {
101            nonce,
102            invitation_bytes: None,
103            error: Some(message.into()),
104        }
105    }
106}
107
108/// Codec for specialized node invite request-response protocol
109#[derive(Debug, Clone, Default)]
110pub struct SpecializedNodeInviteCodec;
111
112#[async_trait]
113impl Codec for SpecializedNodeInviteCodec {
114    type Protocol = StreamProtocol;
115    type Request = VerificationRequest;
116    type Response = SpecializedNodeInvitationResponse;
117
118    async fn read_request<T>(
119        &mut self,
120        _protocol: &Self::Protocol,
121        io: &mut T,
122    ) -> io::Result<Self::Request>
123    where
124        T: AsyncRead + Unpin + Send,
125    {
126        // Read length prefix (4 bytes, big endian)
127        let mut len_buf = [0u8; 4];
128        io.read_exact(&mut len_buf).await?;
129        let len = u32::from_be_bytes(len_buf) as usize;
130
131        if len as u64 > MAX_SPECIALIZED_NODE_INVITE_MESSAGE_SIZE {
132            return Err(io::Error::new(
133                io::ErrorKind::InvalidData,
134                "request too large",
135            ));
136        }
137
138        // Read the message bytes
139        let mut buf = vec![0u8; len];
140        io.read_exact(&mut buf).await?;
141
142        // Deserialize
143        borsh::from_slice(&buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
144    }
145
146    async fn read_response<T>(
147        &mut self,
148        _protocol: &Self::Protocol,
149        io: &mut T,
150    ) -> io::Result<Self::Response>
151    where
152        T: AsyncRead + Unpin + Send,
153    {
154        // Read length prefix (4 bytes, big endian)
155        let mut len_buf = [0u8; 4];
156        io.read_exact(&mut len_buf).await?;
157        let len = u32::from_be_bytes(len_buf) as usize;
158
159        if len as u64 > MAX_SPECIALIZED_NODE_INVITE_MESSAGE_SIZE {
160            return Err(io::Error::new(
161                io::ErrorKind::InvalidData,
162                "response too large",
163            ));
164        }
165
166        // Read the message bytes
167        let mut buf = vec![0u8; len];
168        io.read_exact(&mut buf).await?;
169
170        // Deserialize
171        borsh::from_slice(&buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
172    }
173
174    async fn write_request<T>(
175        &mut self,
176        _protocol: &Self::Protocol,
177        io: &mut T,
178        req: Self::Request,
179    ) -> io::Result<()>
180    where
181        T: AsyncWrite + Unpin + Send,
182    {
183        // Serialize
184        let buf = borsh::to_vec(&req).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
185
186        // Write length prefix
187        let len = buf.len() as u32;
188        io.write_all(&len.to_be_bytes()).await?;
189
190        // Write message
191        io.write_all(&buf).await?;
192        io.flush().await?;
193
194        Ok(())
195    }
196
197    async fn write_response<T>(
198        &mut self,
199        _protocol: &Self::Protocol,
200        io: &mut T,
201        res: Self::Response,
202    ) -> io::Result<()>
203    where
204        T: AsyncWrite + Unpin + Send,
205    {
206        // Serialize
207        let buf = borsh::to_vec(&res).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
208
209        // Write length prefix
210        let len = buf.len() as u32;
211        io.write_all(&len.to_be_bytes()).await?;
212
213        // Write message
214        io.write_all(&buf).await?;
215        io.flush().await?;
216
217        Ok(())
218    }
219}