1use crate::GtpMessage;
4use gbp::CodecError;
5use gbp_core::{BoundedSeen, GbpFlags, MemberId, PayloadCodec, StreamType};
6use gbp_node::{GroupNode, NodeError, OutboundFrame, Sealer};
7
8#[derive(Debug, thiserror::Error)]
10pub enum GtpError {
11 #[error("decode: {0}")]
13 Decode(#[from] CodecError),
14 #[error("duplicate (sender={sender_id}, mid=0x{message_id:X})")]
16 Duplicate {
17 sender_id: MemberId,
19 message_id: u64,
21 },
22 #[error("node: {0}")]
24 Node(#[from] NodeError),
25}
26
27#[derive(Debug)]
29pub enum GtpAccept {
30 New(GtpMessage),
32 Duplicate(GtpMessage),
34}
35
36const GTP_SEEN_CAP: usize = 10_000;
38
39pub struct GtpClient {
51 seen: BoundedSeen<(MemberId, u64)>,
52 current_epoch: Option<u64>,
53}
54
55impl Default for GtpClient {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl GtpClient {
62 pub fn new() -> Self {
64 Self {
65 seen: BoundedSeen::new(GTP_SEEN_CAP),
66 current_epoch: None,
67 }
68 }
69
70 pub fn send<S: Sealer>(
77 &mut self,
78 node: &mut GroupNode,
79 seal: &mut S,
80 target: MemberId,
81 message_id: u64,
82 text: &str,
83 codec: PayloadCodec,
84 ) -> Result<OutboundFrame, GtpError> {
85 self.sync_epoch(node.current_epoch);
86 let msg = GtpMessage::plain(node.member_id, message_id, text);
87 let stream_id = node.member_stream_id(1);
88 let of = node.send_payload(
89 seal,
90 target,
91 StreamType::Text,
92 stream_id,
93 GbpFlags::ordered_reliable_ack(),
94 &msg.to_bytes(codec),
95 codec,
96 )?;
97 Ok(of)
98 }
99
100 pub fn accept(
109 &mut self,
110 plaintext: &[u8],
111 current_epoch: u64,
112 codec: PayloadCodec,
113 ) -> Result<GtpAccept, GtpError> {
114 self.sync_epoch(current_epoch);
115 let m = GtpMessage::from_bytes(plaintext, codec)?;
116 let key = (m.sender_id, m.message_id);
117 if !self.seen.insert(key) {
118 return Ok(GtpAccept::Duplicate(m));
119 }
120 Ok(GtpAccept::New(m))
121 }
122
123 pub fn sync_epoch(&mut self, epoch: u64) {
127 if Some(epoch) != self.current_epoch {
128 self.seen.clear();
129 self.current_epoch = Some(epoch);
130 }
131 }
132
133 pub fn reset(&mut self) {
135 self.seen.clear();
136 self.current_epoch = None;
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::GtpMessage;
144
145 fn encode_msg(sender_id: u32, message_id: u64) -> Vec<u8> {
146 GtpMessage::plain(sender_id, message_id, "hello").to_cbor()
147 }
148
149 #[test]
150 fn accept_new_message_returns_new() {
151 let mut client = GtpClient::new();
152 let payload = encode_msg(1, 100);
153 assert!(matches!(
154 client.accept(&payload, 0, PayloadCodec::Cbor).unwrap(),
155 GtpAccept::New(_)
156 ));
157 }
158
159 #[test]
160 fn accept_duplicate_returns_duplicate() {
161 let mut client = GtpClient::new();
162 let payload = encode_msg(1, 100);
163 client.accept(&payload, 0, PayloadCodec::Cbor).unwrap();
164 let result = client.accept(&payload, 0, PayloadCodec::Cbor).unwrap();
165 assert!(matches!(result, GtpAccept::Duplicate(_)));
166 }
167
168 #[test]
169 fn different_message_ids_both_new() {
170 let mut client = GtpClient::new();
171 let p1 = encode_msg(1, 1);
172 let p2 = encode_msg(1, 2);
173 assert!(matches!(
174 client.accept(&p1, 0, PayloadCodec::Cbor).unwrap(),
175 GtpAccept::New(_)
176 ));
177 assert!(matches!(
178 client.accept(&p2, 0, PayloadCodec::Cbor).unwrap(),
179 GtpAccept::New(_)
180 ));
181 }
182
183 #[test]
184 fn different_senders_same_message_id_both_new() {
185 let mut client = GtpClient::new();
186 let p1 = encode_msg(1, 42);
187 let p2 = encode_msg(2, 42);
188 assert!(matches!(
189 client.accept(&p1, 0, PayloadCodec::Cbor).unwrap(),
190 GtpAccept::New(_)
191 ));
192 assert!(matches!(
193 client.accept(&p2, 0, PayloadCodec::Cbor).unwrap(),
194 GtpAccept::New(_)
195 ));
196 }
197
198 #[test]
199 fn epoch_advance_clears_seen_set() {
200 let mut client = GtpClient::new();
201 let payload = encode_msg(1, 100);
202 client.accept(&payload, 0, PayloadCodec::Cbor).unwrap();
203 let result = client.accept(&payload, 1, PayloadCodec::Cbor).unwrap();
205 assert!(matches!(result, GtpAccept::New(_)));
206 }
207
208 #[test]
209 fn reset_clears_idempotency_state() {
210 let mut client = GtpClient::new();
211 let payload = encode_msg(7, 999);
212 client.accept(&payload, 5, PayloadCodec::Cbor).unwrap();
213 client.reset();
214 let result = client.accept(&payload, 5, PayloadCodec::Cbor).unwrap();
215 assert!(matches!(result, GtpAccept::New(_)));
216 }
217
218 #[test]
219 fn sync_epoch_same_value_keeps_state() {
220 let mut client = GtpClient::new();
221 let payload = encode_msg(1, 1);
222 client.accept(&payload, 3, PayloadCodec::Cbor).unwrap();
223 client.sync_epoch(3); let result = client.accept(&payload, 3, PayloadCodec::Cbor).unwrap();
225 assert!(matches!(result, GtpAccept::Duplicate(_)));
226 }
227
228 #[test]
229 fn invalid_cbor_returns_decode_error() {
230 let mut client = GtpClient::new();
231 let result = client.accept(b"\xFF\xFF", 0, PayloadCodec::Cbor);
232 assert!(matches!(result, Err(GtpError::Decode(_))));
233 }
234}