Skip to main content

nox_core/models/
payloads.rs

1use serde::{Deserialize, Serialize};
2
3use crate::protocol::fragmentation::Fragment;
4use nox_crypto::sphinx::surb::Surb;
5
6/// Current payload wire version. Prepended as a 1-byte prefix to all bincode payloads.
7pub const PAYLOAD_VERSION: u8 = 1;
8
9/// Encode a value for wire transport: `[version: u8][bincode body: ...]`.
10pub fn encode_payload<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
11    let body = bincode::serialize(value).map_err(|e| e.to_string())?;
12    let mut out = Vec::with_capacity(1 + body.len());
13    out.push(PAYLOAD_VERSION);
14    out.extend_from_slice(&body);
15    Ok(out)
16}
17
18/// Decode a versioned wire payload back into `T`.
19pub fn decode_payload<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, String> {
20    match bytes.split_first() {
21        None => Err("empty payload bytes".into()),
22        Some((&ver, body)) => {
23            if ver != PAYLOAD_VERSION {
24                return Err(format!("unsupported payload version {ver}"));
25            }
26            bincode::deserialize(body).map_err(|e| e.to_string())
27        }
28    }
29}
30
31/// Like `decode_payload` but with a bincode size limit to prevent OOM from malicious packets.
32pub fn decode_payload_limited<T: for<'de> Deserialize<'de>>(
33    bytes: &[u8],
34    max_bytes: u64,
35) -> Result<T, String> {
36    use bincode::Options;
37    match bytes.split_first() {
38        None => Err("empty payload bytes".into()),
39        Some((&ver, body)) => {
40            if ver != PAYLOAD_VERSION {
41                return Err(format!("unsupported payload version {ver}"));
42            }
43            bincode::DefaultOptions::new()
44                .with_limit(max_bytes)
45                .with_fixint_encoding()
46                .allow_trailing_bytes()
47                .deserialize(body)
48                .map_err(|e| e.to_string())
49        }
50    }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub enum RelayerPayload {
55    /// `to` is raw 20-byte Ethereum address (no hex encoding overhead on wire).
56    SubmitTransaction {
57        to: [u8; 20],
58        data: Vec<u8>,
59    },
60    Dummy {
61        padding: Vec<u8>,
62    },
63    Heartbeat {
64        id: u64,
65        timestamp: u64,
66    },
67    Fragment {
68        frag: Fragment,
69    },
70    AnonymousRequest {
71        inner: Vec<u8>,
72        reply_surbs: Vec<Surb>,
73    },
74    ServiceResponse {
75        request_id: u64,
76        fragment: Fragment,
77    },
78    /// Exit node exhausted reply SURBs; sent in the last available SURB so the
79    /// client can deliver a fresh batch for the exit to resume sending.
80    NeedMoreSurbs {
81        request_id: u64,
82        fragments_remaining: u32,
83    },
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum ServiceRequest {
88    Echo {
89        data: Vec<u8>,
90    },
91    HttpRequest {
92        method: String,
93        url: String,
94        headers: Vec<(String, String)>,
95        body: Vec<u8>,
96    },
97    /// Anonymous JSON-RPC query. `rpc_url: None` uses node default (read-only whitelist enforced).
98    RpcRequest {
99        method: String,
100        params: Vec<u8>,
101        id: u64,
102        rpc_url: Option<String>,
103    },
104    SubmitTransaction {
105        to: [u8; 20],
106        data: Vec<u8>,
107    },
108    /// Broadcast a pre-signed transaction. Client pays gas; no relayer signing or profitability check.
109    BroadcastSignedTransaction {
110        signed_tx: Vec<u8>,
111        rpc_url: Option<String>,
112        /// JSON-RPC method name (None = `eth_sendRawTransaction`)
113        rpc_method: Option<String>,
114    },
115    /// Client sends fresh SURBs to let a stalled exit node resume response delivery.
116    ReplenishSurbs {
117        request_id: u64,
118        surbs: Vec<nox_crypto::sphinx::surb::Surb>,
119    },
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct RpcResponse {
124    pub id: u64,
125    pub result: Result<Vec<u8>, String>,
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_broadcast_signed_transaction_roundtrip() {
134        let fake_signed_tx = vec![0xf8, 0x65, 0x80, 0x84, 0x3b, 0x9a, 0xca, 0x00];
135        let request = ServiceRequest::BroadcastSignedTransaction {
136            signed_tx: fake_signed_tx.clone(),
137            rpc_url: None,
138            rpc_method: None,
139        };
140
141        let encoded = encode_payload(&request).expect("encode");
142        assert_eq!(encoded[0], PAYLOAD_VERSION);
143
144        let decoded: ServiceRequest = decode_payload(&encoded).expect("decode");
145
146        match decoded {
147            ServiceRequest::BroadcastSignedTransaction {
148                signed_tx,
149                rpc_url,
150                rpc_method,
151            } => {
152                assert_eq!(signed_tx, fake_signed_tx);
153                assert!(rpc_url.is_none());
154                assert!(rpc_method.is_none());
155            }
156            other => panic!("expected BroadcastSignedTransaction, got {other:?}"),
157        }
158    }
159
160    #[test]
161    fn test_broadcast_with_custom_rpc_roundtrip() {
162        let request = ServiceRequest::BroadcastSignedTransaction {
163            signed_tx: vec![0xf8, 0x65],
164            rpc_url: Some("https://rpc.ankr.com/eth".to_string()),
165            rpc_method: Some("sendTransaction".to_string()),
166        };
167
168        let encoded = encode_payload(&request).expect("encode");
169        let decoded: ServiceRequest = decode_payload(&encoded).expect("decode");
170
171        match decoded {
172            ServiceRequest::BroadcastSignedTransaction {
173                signed_tx,
174                rpc_url,
175                rpc_method,
176            } => {
177                assert_eq!(signed_tx, vec![0xf8, 0x65]);
178                assert_eq!(rpc_url.as_deref(), Some("https://rpc.ankr.com/eth"));
179                assert_eq!(rpc_method.as_deref(), Some("sendTransaction"));
180            }
181            other => panic!("expected BroadcastSignedTransaction, got {other:?}"),
182        }
183    }
184
185    #[test]
186    fn test_relayer_payload_roundtrip() {
187        let payload = RelayerPayload::Heartbeat {
188            id: 42,
189            timestamp: 1_700_000_000,
190        };
191        let encoded = encode_payload(&payload).expect("encode");
192        assert_eq!(encoded[0], PAYLOAD_VERSION);
193        let decoded: RelayerPayload = decode_payload(&encoded).expect("decode");
194        match decoded {
195            RelayerPayload::Heartbeat { id, timestamp } => {
196                assert_eq!(id, 42);
197                assert_eq!(timestamp, 1_700_000_000);
198            }
199            other => panic!("expected Heartbeat, got {other:?}"),
200        }
201    }
202
203    #[test]
204    fn test_decode_rejects_unsupported_version() {
205        // Build a packet with a version byte of 99.
206        let mut bad = vec![99u8];
207        bad.extend_from_slice(
208            &bincode::serialize(&ServiceRequest::Echo { data: vec![1] }).unwrap(),
209        );
210        let result: Result<ServiceRequest, _> = decode_payload(&bad);
211        assert!(result.is_err());
212        assert!(result.unwrap_err().contains("unsupported payload version"));
213    }
214
215    #[test]
216    fn test_decode_rejects_empty_bytes() {
217        let result: Result<ServiceRequest, _> = decode_payload(&[]);
218        assert!(result.is_err());
219    }
220
221    /// Verify TS SDK bincode encoding matches Rust (cross-language parity).
222    #[test]
223    fn test_ts_sdk_anonymous_request_decode() {
224        let ts_bytes: Vec<u8> = vec![
225            0x01, 0x04, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
226            0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0x45,
227            0x54, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x74, 0x74, 0x70, 0x73,
228            0x3a, 0x2f, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x62, 0x69, 0x6e, 0x2e, 0x6f, 0x72, 0x67,
229            0x2f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x2f, 0x31, 0x30, 0x32, 0x34, 0x00, 0x00, 0x00,
230            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
231            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
232        ];
233        assert_eq!(ts_bytes.len(), 91);
234
235        // Rust's own encoding of the same value
236        let rust_inner = encode_payload(&ServiceRequest::HttpRequest {
237            method: "GET".to_string(),
238            url: "https://httpbin.org/bytes/1024".to_string(),
239            headers: vec![],
240            body: vec![],
241        })
242        .unwrap();
243        let rust_bytes = encode_payload(&RelayerPayload::AnonymousRequest {
244            inner: rust_inner,
245            reply_surbs: vec![],
246        })
247        .unwrap();
248
249        assert_eq!(ts_bytes, rust_bytes, "TS SDK / Rust encoding mismatch");
250
251        // Decode and verify the payload structure
252        let payload = decode_payload_limited::<RelayerPayload>(&ts_bytes, 65536)
253            .expect("TS SDK bytes must decode successfully");
254        match payload {
255            RelayerPayload::AnonymousRequest { inner, reply_surbs } => {
256                assert!(reply_surbs.is_empty());
257                let sr = decode_payload_limited::<ServiceRequest>(&inner, 65536)
258                    .expect("inner ServiceRequest must decode");
259                match sr {
260                    ServiceRequest::HttpRequest {
261                        method,
262                        url,
263                        headers,
264                        body,
265                    } => {
266                        assert_eq!(method, "GET");
267                        assert_eq!(url, "https://httpbin.org/bytes/1024");
268                        assert!(headers.is_empty());
269                        assert!(body.is_empty());
270                    }
271                    other => panic!(
272                        "expected HttpRequest, got variant {:?}",
273                        std::mem::discriminant(&other)
274                    ),
275                }
276            }
277            other => panic!(
278                "expected AnonymousRequest, got variant {:?}",
279                std::mem::discriminant(&other)
280            ),
281        }
282    }
283}