use bytes::{Buf, BufMut, BytesMut};
use crate::{anyhow_assert_eq, anyhow_assert_ge};
use crate::tools::config::CLIENT_FEEDBACK_POW_NUMERAIRE;
use crate::tools::pow_generator::pow_generator::PowGenerator;
use crate::tools::pow;
use crate::tools::types::{Hash, Id, Pow, Salt, ID_BYTES, SALT_BYTES};
const OFF_POST_ID: usize = 1;
const OFF_FEEDBACK_TYPE: usize = OFF_POST_ID + ID_BYTES;
const OFF_SALT: usize = OFF_FEEDBACK_TYPE + 1;
const OFF_POW: usize = OFF_SALT + SALT_BYTES;
pub const ENTRY_SIZE: usize = OFF_POW + 1;
pub struct EncodedPostFeedbackViewV1<'a>(&'a [u8]);
impl<'a> EncodedPostFeedbackViewV1<'a> {
pub fn from_slice(bytes: &'a [u8]) -> anyhow::Result<Self> {
anyhow_assert_eq!(bytes.len(), ENTRY_SIZE, "wrong entry size for EncodedPostFeedbackViewV1");
anyhow_assert_eq!(bytes[0], 1u8, "unsupported version in EncodedPostFeedbackViewV1");
Ok(Self(bytes))
}
pub fn post_id_bytes(&self) -> &'a [u8] {
&self.0[OFF_POST_ID..OFF_FEEDBACK_TYPE]
}
pub fn feedback_type(&self) -> u8 {
self.0[OFF_FEEDBACK_TYPE]
}
pub fn salt_bytes(&self) -> &'a [u8] {
&self.0[OFF_SALT..OFF_POW]
}
pub fn pow(&self) -> Pow {
Pow(self.0[OFF_POW])
}
pub fn iter(bytes: &'a [u8]) -> impl Iterator<Item = anyhow::Result<Self>> + 'a {
bytes.chunks_exact(ENTRY_SIZE).map(Self::from_slice)
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct EncodedPostFeedbackV1 {
pub post_id: Id,
pub feedback_type: u8,
pub salt: Salt,
pub pow: Pow,
}
impl EncodedPostFeedbackV1 {
pub fn new(post_id: Id, feedback_type: u8, salt: Salt, pow: Pow) -> Self {
Self {
post_id,
feedback_type,
salt,
pow,
}
}
pub async fn pow_generate(post_id: &Id, feedback_type: u8, pow_generator: &dyn PowGenerator) -> anyhow::Result<(Salt, Pow, Hash)> {
let data_hash = pow::pow_compute_data_hash(&[post_id.as_bytes(), &[feedback_type]]);
pow_generator.generate_best_effort_tracked("feedback", CLIENT_FEEDBACK_POW_NUMERAIRE, Pow(255), data_hash).await
}
pub fn pow_verify(&self) -> anyhow::Result<()> {
let (pow, _hash) = pow::pow_measure(&[self.post_id.as_bytes(), &[self.feedback_type]], &self.salt)?;
anyhow_assert_eq!(pow, self.pow);
Ok(())
}
pub async fn encode_to_bytes(&mut self) -> anyhow::Result<Vec<u8>> {
let mut bytes = BytesMut::new();
bytes.put_u8(1); bytes.put_slice(self.post_id.as_ref());
bytes.put_u8(self.feedback_type);
bytes.put_slice(self.salt.as_ref());
bytes.put_u8(self.pow.0);
let bytes = bytes.to_vec();
Ok(bytes)
}
pub fn append_encode_direct_to_bytes<B: BufMut>(bytes: &mut B, post_id: &[u8], feedback_type: u8, salt: &[u8], pow: Pow) -> anyhow::Result<()> {
anyhow_assert_eq!(post_id.len(), ID_BYTES);
anyhow_assert_eq!(salt.len(), SALT_BYTES);
bytes.put_u8(1); bytes.put_slice(post_id.as_ref());
bytes.put_u8(feedback_type);
bytes.put_slice(salt.as_ref());
bytes.put_u8(pow.0);
Ok(())
}
pub fn append_encode_to_bytes<B: BufMut>(&self, bytes: &mut B) -> anyhow::Result<()> {
Self::append_encode_direct_to_bytes(bytes, self.post_id.as_ref(), self.feedback_type, self.salt.as_ref(), self.pow)
}
pub fn decode_from_bytes(mut bytes: impl Buf) -> anyhow::Result<Self> {
anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
let version = bytes.get_u8();
anyhow_assert_eq!(1, version);
let post_id = Id::from_buf(&mut bytes, "post_id")?;
anyhow_assert_ge!(bytes.remaining(), 1, "Missing feedback_type");
let feedback_type = bytes.get_u8();
let salt = Salt::from_buf(&mut bytes, "salt")?;
anyhow_assert_ge!(bytes.remaining(), 1, "Missing pow");
let pow = Pow(bytes.get_u8());
Ok(Self {
post_id,
feedback_type,
salt,
pow,
})
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
use crate::tools::types::{Id, Salt};
const ENTRY_SIZE: usize = 1 + ID_BYTES + 1 + SALT_BYTES + 1;
#[tokio::test]
async fn roundtrip_encode_decode() -> anyhow::Result<()> {
let original = EncodedPostFeedbackV1::new(Id::random(), 3, Salt::random(), Pow(42));
let bytes = original.clone().encode_to_bytes().await?;
assert_eq!(bytes.len(), ENTRY_SIZE);
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes.clone()))?;
assert_eq!(original, decoded);
let bytes2 = decoded.clone().encode_to_bytes().await?;
assert_eq!(bytes, bytes2);
Ok(())
}
#[tokio::test]
async fn roundtrip_all_feedback_types() -> anyhow::Result<()> {
for feedback_type in [0u8, 1, 2, 3, 127, 255] {
let original = EncodedPostFeedbackV1::new(Id::random(), feedback_type, Salt::random(), Pow(0));
let bytes = original.clone().encode_to_bytes().await?;
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes))?;
assert_eq!(original, decoded, "failed for feedback_type={feedback_type}");
}
Ok(())
}
#[tokio::test]
async fn roundtrip_extreme_pow_values() -> anyhow::Result<()> {
for pow in [0u8, 1, 127, 255] {
let original = EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(pow));
let bytes = original.clone().encode_to_bytes().await?;
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes))?;
assert_eq!(original, decoded, "failed for pow={pow}");
}
Ok(())
}
#[test]
fn append_single_roundtrip() -> anyhow::Result<()> {
let original = EncodedPostFeedbackV1::new(Id::random(), 5, Salt::random(), Pow(99));
let mut buf = Vec::new();
EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, original.post_id.as_ref(), original.feedback_type, original.salt.as_ref(), original.pow)?;
assert_eq!(buf.len(), ENTRY_SIZE);
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(buf))?;
assert_eq!(original, decoded);
Ok(())
}
#[test]
fn append_multiple_roundtrip() -> anyhow::Result<()> {
let originals = [
EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(10)),
EncodedPostFeedbackV1::new(Id::random(), 2, Salt::random(), Pow(20)),
EncodedPostFeedbackV1::new(Id::random(), 255, Salt::zero(), Pow(0)),
];
let mut buf = Vec::new();
for f in &originals {
EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, f.post_id.as_ref(), f.feedback_type, f.salt.as_ref(), f.pow)?;
}
assert_eq!(buf.len(), originals.len() * ENTRY_SIZE);
for (i, expected) in originals.iter().enumerate() {
let start = i * ENTRY_SIZE;
let mut chunk = Bytes::copy_from_slice(&buf[start..start + ENTRY_SIZE]);
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut chunk)?;
assert_eq!(*expected, decoded, "mismatch at entry {i}");
}
Ok(())
}
#[test]
fn append_rejects_wrong_post_id_length() {
let mut buf = Vec::new();
let short_id = vec![0u8; ID_BYTES - 1];
let salt = Salt::zero();
assert!(EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, &short_id, 1, salt.as_ref(), Pow(0)).is_err());
}
#[test]
fn append_rejects_wrong_salt_length() {
let mut buf = Vec::new();
let id = Id::random();
let short_salt = vec![0u8; SALT_BYTES - 1];
assert!(EncodedPostFeedbackV1::append_encode_direct_to_bytes(&mut buf, id.as_ref(), 1, &short_salt, Pow(0)).is_err());
}
#[test]
fn decode_rejects_empty_input() {
assert!(EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::new()).is_err());
}
#[test]
fn decode_rejects_wrong_version() {
let mut buf = vec![0u8; ENTRY_SIZE];
buf[0] = 99; assert!(EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(buf)).is_err());
}
#[test]
fn decode_rejects_truncated_at_each_boundary() {
let valid = {
let mut b = BytesMut::new();
b.put_u8(1);
b.put_slice(Id::random().as_ref());
b.put_u8(3);
b.put_slice(Salt::random().as_ref());
b.put_u8(7);
b.freeze()
};
assert_eq!(valid.len(), ENTRY_SIZE);
for truncate_at in 0..ENTRY_SIZE {
let mut truncated = valid.slice(0..truncate_at);
assert!(
EncodedPostFeedbackV1::decode_from_bytes(&mut truncated).is_err(),
"expected error when truncated to {truncate_at} bytes"
);
}
}
#[tokio::test]
async fn view_matches_encoder_and_decoder() -> anyhow::Result<()> {
let original = EncodedPostFeedbackV1::new(Id::random(), 7, Salt::random(), Pow(42));
let bytes_for_view = original.clone().encode_to_bytes().await?;
let bytes_for_decode = bytes_for_view.clone();
let view = EncodedPostFeedbackViewV1::from_slice(&bytes_for_view)?;
assert_eq!(view.post_id_bytes(), original.post_id.as_ref());
assert_eq!(view.feedback_type(), original.feedback_type);
assert_eq!(view.salt_bytes(), original.salt.as_ref());
assert_eq!(view.pow(), original.pow);
let decoded = EncodedPostFeedbackV1::decode_from_bytes(&mut Bytes::from(bytes_for_decode))?;
assert_eq!(view.post_id_bytes(), decoded.post_id.as_ref());
assert_eq!(view.feedback_type(), decoded.feedback_type);
assert_eq!(view.salt_bytes(), decoded.salt.as_ref());
assert_eq!(view.pow(), decoded.pow);
Ok(())
}
#[test]
fn view_iter_matches_originals() -> anyhow::Result<()> {
let originals = [
EncodedPostFeedbackV1::new(Id::random(), 1, Salt::random(), Pow(10)),
EncodedPostFeedbackV1::new(Id::random(), 2, Salt::random(), Pow(20)),
EncodedPostFeedbackV1::new(Id::random(), 255, Salt::zero(), Pow(0)),
];
let mut buf = Vec::new();
for f in &originals {
f.append_encode_to_bytes(&mut buf)?;
}
let views: Vec<_> = EncodedPostFeedbackViewV1::iter(&buf)
.collect::<anyhow::Result<_>>()?;
assert_eq!(views.len(), originals.len());
for (view, original) in views.iter().zip(originals.iter()) {
assert_eq!(view.post_id_bytes(), original.post_id.as_ref());
assert_eq!(view.feedback_type(), original.feedback_type);
assert_eq!(view.salt_bytes(), original.salt.as_ref());
assert_eq!(view.pow(), original.pow);
}
Ok(())
}
#[test]
fn view_iter_ignores_partial_tail() -> anyhow::Result<()> {
let f = EncodedPostFeedbackV1::new(Id::random(), 3, Salt::random(), Pow(5));
let mut buf = Vec::new();
f.append_encode_to_bytes(&mut buf)?;
buf.push(0xFF);
let views: Vec<_> = EncodedPostFeedbackViewV1::iter(&buf)
.collect::<anyhow::Result<_>>()?;
assert_eq!(views.len(), 1);
Ok(())
}
#[test]
fn view_rejects_wrong_length() {
assert!(EncodedPostFeedbackViewV1::from_slice(&[]).is_err());
assert!(EncodedPostFeedbackViewV1::from_slice(&vec![1u8; ENTRY_SIZE - 1]).is_err());
assert!(EncodedPostFeedbackViewV1::from_slice(&vec![1u8; ENTRY_SIZE + 1]).is_err());
}
#[test]
fn view_rejects_wrong_version() {
let mut buf = vec![1u8; ENTRY_SIZE];
buf[0] = 99;
assert!(EncodedPostFeedbackViewV1::from_slice(&buf).is_err());
}
#[cfg(not(target_arch = "wasm32"))]
mod bolero_fuzz {
use bytes::Bytes;
use crate::protocol::posting::encoded_post_feedback::EncodedPostFeedbackV1;
#[test]
fn fuzz_decode_from_bytes() {
bolero::check!().for_each(|data: &[u8]| {
let mut bytes = Bytes::copy_from_slice(data);
let _ = EncodedPostFeedbackV1::decode_from_bytes(&mut bytes);
});
}
}
}