hashiverse_lib/protocol/posting/
encoded_post_feedback.rs1use bytes::{Buf, BufMut, BytesMut};
18use crate::{anyhow_assert_eq, anyhow_assert_ge};
19use crate::tools::config::CLIENT_FEEDBACK_POW_NUMERAIRE;
20use crate::tools::parallel_pow_generator::ParallelPowGenerator;
21use crate::tools::pow;
22use crate::tools::types::{Hash, Id, Pow, Salt, ID_BYTES, SALT_BYTES};
23
24const OFF_POST_ID: usize = 1;
26const OFF_FEEDBACK_TYPE: usize = OFF_POST_ID + ID_BYTES;
27const OFF_SALT: usize = OFF_FEEDBACK_TYPE + 1;
28const OFF_POW: usize = OFF_SALT + SALT_BYTES;
29pub const ENTRY_SIZE: usize = OFF_POW + 1; pub struct EncodedPostFeedbackViewV1<'a>(&'a [u8]);
35
36impl<'a> EncodedPostFeedbackViewV1<'a> {
37 pub fn from_slice(bytes: &'a [u8]) -> anyhow::Result<Self> {
38 anyhow_assert_eq!(bytes.len(), ENTRY_SIZE, "wrong entry size for EncodedPostFeedbackViewV1");
39 anyhow_assert_eq!(bytes[0], 1u8, "unsupported version in EncodedPostFeedbackViewV1");
40 Ok(Self(bytes))
41 }
42
43 pub fn post_id_bytes(&self) -> &'a [u8] {
44 &self.0[OFF_POST_ID..OFF_FEEDBACK_TYPE]
45 }
46
47 pub fn feedback_type(&self) -> u8 {
48 self.0[OFF_FEEDBACK_TYPE]
49 }
50
51 pub fn salt_bytes(&self) -> &'a [u8] {
52 &self.0[OFF_SALT..OFF_POW]
53 }
54
55 pub fn pow(&self) -> Pow {
56 Pow(self.0[OFF_POW])
57 }
58
59 pub fn iter(bytes: &'a [u8]) -> impl Iterator<Item = anyhow::Result<Self>> + 'a {
63 bytes.chunks_exact(ENTRY_SIZE).map(Self::from_slice)
64 }
65}
66
67#[derive(Debug, PartialEq, Clone)]
68pub struct EncodedPostFeedbackV1 {
69 pub post_id: Id,
70 pub feedback_type: u8,
71 pub salt: Salt,
72 pub pow: Pow,
73}
74impl EncodedPostFeedbackV1 {
75 pub fn new(post_id: Id, feedback_type: u8, salt: Salt, pow: Pow) -> Self {
76 Self {
77 post_id,
78 feedback_type,
79 salt,
80 pow,
81 }
82 }
83
84 pub async fn pow_generate(post_id: &Id, feedback_type: u8, pow_generator: &dyn ParallelPowGenerator) -> anyhow::Result<(Salt, Pow, Hash)> {
85 let data_hash = pow::pow_compute_data_hash(&[post_id.as_bytes(), &[feedback_type]]);
86 pow_generator.generate_best_effort("feedback", CLIENT_FEEDBACK_POW_NUMERAIRE, Pow(255), data_hash).await
87 }
88
89 pub fn pow_verify(&self) -> anyhow::Result<()> {
90 let (pow, _hash) = pow::pow_measure(&[self.post_id.as_bytes(), &[self.feedback_type]], &self.salt)?;
91 anyhow_assert_eq!(pow, self.pow);
92 Ok(())
93 }
94
95 pub async fn encode_to_bytes(&mut self) -> anyhow::Result<Vec<u8>> {
96 let mut bytes = BytesMut::new();
97 bytes.put_u8(1); bytes.put_slice(self.post_id.as_ref());
99 bytes.put_u8(self.feedback_type);
100 bytes.put_slice(self.salt.as_ref());
101 bytes.put_u8(self.pow.0);
102 let bytes = bytes.to_vec();
103 Ok(bytes)
104 }
105
106 pub fn append_encode_direct_to_bytes<B: BufMut>(bytes: &mut B, post_id: &[u8], feedback_type: u8, salt: &[u8], pow: Pow) -> anyhow::Result<()> {
107 anyhow_assert_eq!(post_id.len(), ID_BYTES);
108 anyhow_assert_eq!(salt.len(), SALT_BYTES);
109
110 bytes.put_u8(1); bytes.put_slice(post_id.as_ref());
112 bytes.put_u8(feedback_type);
113 bytes.put_slice(salt.as_ref());
114 bytes.put_u8(pow.0);
115
116 Ok(())
117 }
118
119 pub fn append_encode_to_bytes<B: BufMut>(&self, bytes: &mut B) -> anyhow::Result<()> {
120 Self::append_encode_direct_to_bytes(bytes, self.post_id.as_ref(), self.feedback_type, self.salt.as_ref(), self.pow)
121 }
122
123 pub fn decode_from_bytes(mut bytes: impl Buf) -> anyhow::Result<Self> {
124 anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
125 let version = bytes.get_u8();
126 anyhow_assert_eq!(1, version);
127
128 let post_id = Id::from_buf(&mut bytes, "post_id")?;
129
130 anyhow_assert_ge!(bytes.remaining(), 1, "Missing feedback_type");
131 let feedback_type = bytes.get_u8();
132
133 let salt = Salt::from_buf(&mut bytes, "salt")?;
134
135 anyhow_assert_ge!(bytes.remaining(), 1, "Missing pow");
136 let pow = Pow(bytes.get_u8());
137
138 Ok(Self {
141 post_id,
142 feedback_type,
143 salt,
144 pow,
145 })
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use bytes::Bytes;
152 use super::*;
153 use crate::tools::types::{Id, Salt};
154
155 const ENTRY_SIZE: usize = 1 + ID_BYTES + 1 + SALT_BYTES + 1;
156
157 #[tokio::test]
160 async fn roundtrip_encode_decode() -> anyhow::Result<()> {
161 let original = EncodedPostFeedbackV1::new(Id::random(), 3, Salt::random(), Pow(42));
162
163 let bytes = original.clone().encode_to_bytes().await?;
164 assert_eq!(bytes.len(), ENTRY_SIZE);
165
166 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes.clone()))?;
167 assert_eq!(original, decoded);
168
169 let bytes2 = decoded.clone().encode_to_bytes().await?;
171 assert_eq!(bytes, bytes2);
172
173 Ok(())
174 }
175
176 #[tokio::test]
177 async fn roundtrip_all_feedback_types() -> anyhow::Result<()> {
178 for feedback_type in [0u8, 1, 2, 3, 127, 255] {
179 let original = EncodedPostFeedbackV1::new(Id::random(), feedback_type, Salt::random(), Pow(0));
180 let bytes = original.clone().encode_to_bytes().await?;
181 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes))?;
182 assert_eq!(original, decoded, "failed for feedback_type={feedback_type}");
183 }
184 Ok(())
185 }
186
187 #[tokio::test]
188 async fn roundtrip_extreme_pow_values() -> anyhow::Result<()> {
189 for pow in [0u8, 1, 127, 255] {
190 let original = EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(pow));
191 let bytes = original.clone().encode_to_bytes().await?;
192 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes))?;
193 assert_eq!(original, decoded, "failed for pow={pow}");
194 }
195 Ok(())
196 }
197
198 #[test]
201 fn append_single_roundtrip() -> anyhow::Result<()> {
202 let original = EncodedPostFeedbackV1::new(Id::random(), 5, Salt::random(), Pow(99));
203
204 let mut buf = Vec::new();
205 EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, original.post_id.as_ref(), original.feedback_type, original.salt.as_ref(), original.pow)?;
206
207 assert_eq!(buf.len(), ENTRY_SIZE);
208 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(buf))?;
209 assert_eq!(original, decoded);
210 Ok(())
211 }
212
213 #[test]
214 fn append_multiple_roundtrip() -> anyhow::Result<()> {
215 let originals = [
216 EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(10)),
217 EncodedPostFeedbackV1::new(Id::random(), 2, Salt::random(), Pow(20)),
218 EncodedPostFeedbackV1::new(Id::random(), 255, Salt::zero(), Pow(0)),
219 ];
220
221 let mut buf = Vec::new();
222 for f in &originals {
223 EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, f.post_id.as_ref(), f.feedback_type, f.salt.as_ref(), f.pow)?;
224 }
225
226 assert_eq!(buf.len(), originals.len() * ENTRY_SIZE);
227
228 for (i, expected) in originals.iter().enumerate() {
229 let start = i * ENTRY_SIZE;
230 let mut chunk = Bytes::copy_from_slice(&buf[start..start + ENTRY_SIZE]);
231 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut chunk)?;
232 assert_eq!(*expected, decoded, "mismatch at entry {i}");
233 }
234
235 Ok(())
236 }
237
238 #[test]
239 fn append_rejects_wrong_post_id_length() {
240 let mut buf = Vec::new();
241 let short_id = vec![0u8; ID_BYTES - 1];
242 let salt = Salt::zero();
243 assert!(EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, &short_id, 1, salt.as_ref(), Pow(0)).is_err());
244 }
245
246 #[test]
247 fn append_rejects_wrong_salt_length() {
248 let mut buf = Vec::new();
249 let id = Id::random();
250 let short_salt = vec![0u8; SALT_BYTES - 1];
251 assert!(EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, id.as_ref(), 1, &short_salt, Pow(0)).is_err());
252 }
253
254 #[test]
257 fn decode_rejects_empty_input() {
258 assert!(EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::new()).is_err());
259 }
260
261 #[test]
262 fn decode_rejects_wrong_version() {
263 let mut buf = vec![0u8; ENTRY_SIZE];
264 buf[0] = 99; assert!(EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(buf)).is_err());
266 }
267
268 #[test]
269 fn decode_rejects_truncated_at_each_boundary() {
270 let valid = {
272 let mut b = BytesMut::new();
273 b.put_u8(1);
274 b.put_slice(Id::random().as_ref());
275 b.put_u8(3);
276 b.put_slice(Salt::random().as_ref());
277 b.put_u8(7);
278 b.freeze()
279 };
280 assert_eq!(valid.len(), ENTRY_SIZE);
281
282 for truncate_at in 0..ENTRY_SIZE {
283 let mut truncated = valid.slice(0..truncate_at);
284 assert!(
285 EncodedPostFeedbackV1::decode_from_bytes(&mut truncated).is_err(),
286 "expected error when truncated to {truncate_at} bytes"
287 );
288 }
289 }
290
291 #[tokio::test]
296 async fn view_matches_encoder_and_decoder() -> anyhow::Result<()> {
297 let original = EncodedPostFeedbackV1::new(Id::random(), 7, Salt::random(), Pow(42));
298 let bytes_for_view = original.clone().encode_to_bytes().await?;
299 let bytes_for_decode = bytes_for_view.clone();
300
301 let view = EncodedPostFeedbackViewV1::from_slice(&bytes_for_view)?;
302 assert_eq!(view.post_id_bytes(), original.post_id.as_ref());
303 assert_eq!(view.feedback_type(), original.feedback_type);
304 assert_eq!(view.salt_bytes(), original.salt.as_ref());
305 assert_eq!(view.pow(), original.pow);
306
307 let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes_for_decode))?;
309 assert_eq!(view.post_id_bytes(), decoded.post_id.as_ref());
310 assert_eq!(view.feedback_type(), decoded.feedback_type);
311 assert_eq!(view.salt_bytes(), decoded.salt.as_ref());
312 assert_eq!(view.pow(), decoded.pow);
313
314 Ok(())
315 }
316
317 #[test]
320 fn view_iter_matches_originals() -> anyhow::Result<()> {
321 let originals = [
322 EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(10)),
323 EncodedPostFeedbackV1::new(Id::random(), 2, Salt::random(), Pow(20)),
324 EncodedPostFeedbackV1::new(Id::random(), 255, Salt::zero(), Pow(0)),
325 ];
326
327 let mut buf = Vec::new();
328 for f in &originals {
329 f.append_encode_to_bytes(&mut buf)?;
330 }
331
332 let views: Vec<_> = EncodedPostFeedbackViewV1::iter(&buf)
333 .collect::<anyhow::Result<_>>()?;
334
335 assert_eq!(views.len(), originals.len());
336 for (view, original) in views.iter().zip(originals.iter()) {
337 assert_eq!(view.post_id_bytes(), original.post_id.as_ref());
338 assert_eq!(view.feedback_type(), original.feedback_type);
339 assert_eq!(view.salt_bytes(), original.salt.as_ref());
340 assert_eq!(view.pow(), original.pow);
341 }
342
343 Ok(())
344 }
345
346 #[test]
349 fn view_iter_ignores_partial_tail() -> anyhow::Result<()> {
350 let f = EncodedPostFeedbackV1::new(Id::random(), 3, Salt::random(), Pow(5));
351 let mut buf = Vec::new();
352 f.append_encode_to_bytes(&mut buf)?;
353 buf.push(0xFF); let views: Vec<_> = EncodedPostFeedbackViewV1::iter(&buf)
356 .collect::<anyhow::Result<_>>()?;
357 assert_eq!(views.len(), 1);
358
359 Ok(())
360 }
361
362 #[test]
363 fn view_rejects_wrong_length() {
364 assert!(EncodedPostFeedbackViewV1::from_slice(&[]).is_err());
365 assert!(EncodedPostFeedbackViewV1::from_slice(&vec![1u8; ENTRY_SIZE - 1]).is_err());
366 assert!(EncodedPostFeedbackViewV1::from_slice(&vec![1u8; ENTRY_SIZE + 1]).is_err());
367 }
368
369 #[test]
370 fn view_rejects_wrong_version() {
371 let mut buf = vec![1u8; ENTRY_SIZE];
372 buf[0] = 99;
373 assert!(EncodedPostFeedbackViewV1::from_slice(&buf).is_err());
374 }
375
376 #[cfg(not(target_arch = "wasm32"))]
377 mod bolero_fuzz {
378 use bytes::Bytes;
379 use crate::protocol::posting::encoded_post_feedback::EncodedPostFeedbackV1;
380
381 #[test]
382 fn fuzz_decode_from_bytes() {
383 bolero::check!().for_each(|data: &[u8]| {
384 let mut bytes = Bytes::copy_from_slice(data);
385 let _ = EncodedPostFeedbackV1::decode_from_bytes(&mut bytes);
386 });
387 }
388 }
389}