use alloc::vec::Vec;
use nectar_primitives::{AnyChunk, ChunkAddress, DEFAULT_BODY_SIZE, bytes::Bytes};
use crate::{BatchId, STAMP_SIZE, Stamp, StampError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StampedChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
chunk: AnyChunk<BODY_SIZE>,
stamp: Stamp,
}
impl<const BODY_SIZE: usize> StampedChunk<BODY_SIZE> {
#[inline]
#[must_use]
pub const fn new(chunk: AnyChunk<BODY_SIZE>, stamp: Stamp) -> Self {
Self { chunk, stamp }
}
#[inline]
#[must_use]
pub const fn chunk(&self) -> &AnyChunk<BODY_SIZE> {
&self.chunk
}
#[inline]
#[must_use]
pub const fn stamp(&self) -> &Stamp {
&self.stamp
}
#[inline]
#[must_use]
pub fn address(&self) -> &ChunkAddress {
self.chunk.address()
}
#[inline]
#[must_use]
pub fn into_parts(self) -> (AnyChunk<BODY_SIZE>, Stamp) {
(self.chunk, self.stamp)
}
#[must_use]
pub fn to_typed_bytes(&self) -> Vec<u8> {
let stamp = self.stamp.to_bytes();
let chunk = self.chunk.to_typed_bytes();
let mut out = Vec::with_capacity(stamp.len() + chunk.len());
out.extend_from_slice(&stamp);
out.extend_from_slice(&chunk);
out
}
pub fn from_typed_bytes(address: &ChunkAddress, bytes: &[u8]) -> Result<Self, StampError> {
if bytes.len() < STAMP_SIZE {
return Err(StampError::InvalidData(
"stamped chunk shorter than a stamp",
));
}
let (stamp_bytes, chunk_bytes) = bytes.split_at(STAMP_SIZE);
let stamp = Stamp::try_from_slice(stamp_bytes)?;
let chunk = AnyChunk::<BODY_SIZE>::from_typed_bytes(address, chunk_bytes)
.map_err(|_| StampError::Chunk("failed to decode typed chunk"))?;
Ok(Self::new(chunk, stamp))
}
pub fn reconstruct(
expected: ChunkAddress,
data: Bytes,
stamp: Stamp,
) -> Result<Self, StampError> {
let chunk = AnyChunk::<BODY_SIZE>::from_wire_bytes(&expected, data)
.map_err(|_| StampError::Chunk("chunk bytes do not match expected address"))?;
Ok(Self::new(chunk, stamp))
}
pub fn batch_id(typed_bytes: &[u8]) -> Result<BatchId, StampError> {
let id = typed_bytes.get(..32).ok_or(StampError::InvalidData(
"typed bytes shorter than a batch id",
))?;
Ok(BatchId::from_slice(id))
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::{B256, Signature};
use alloy_signer_local::PrivateKeySigner;
use nectar_primitives::{Chunk, ContentChunk, SingleOwnerChunk, bytes::Bytes};
use super::*;
type DefaultStampedChunk = StampedChunk<DEFAULT_BODY_SIZE>;
fn test_stamp() -> Stamp {
let sig = Signature::from_raw(&[1u8; 65]).expect("valid signature");
Stamp::new(B256::repeat_byte(0xaa), 3, 7, 42, sig)
}
fn content_chunk() -> ContentChunk<DEFAULT_BODY_SIZE> {
ContentChunk::new(&b"hello swarm"[..]).expect("valid content chunk")
}
fn single_owner_chunk() -> SingleOwnerChunk<DEFAULT_BODY_SIZE> {
let signer = PrivateKeySigner::from_bytes(&B256::repeat_byte(0x11)).expect("valid signer");
SingleOwnerChunk::new(B256::repeat_byte(0x22), &b"soc payload"[..], &signer)
.expect("valid soc")
}
#[test]
fn into_parts_round_trips_the_fields() {
let chunk: AnyChunk = content_chunk().into();
let stamp = test_stamp();
let stamped = DefaultStampedChunk::new(chunk.clone(), stamp.clone());
assert_eq!(stamped.address(), chunk.address());
let (got_chunk, got_stamp) = stamped.into_parts();
assert_eq!(got_chunk, chunk);
assert_eq!(got_stamp, stamp);
}
#[test]
fn typed_content_round_trip() {
let chunk = content_chunk();
let address = *chunk.address();
let stamp = test_stamp();
let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
let bytes = stamped.to_typed_bytes();
let decoded = DefaultStampedChunk::from_typed_bytes(&address, &bytes).expect("decode");
assert!(decoded.chunk().is_content());
assert_eq!(*decoded.address(), address);
assert_eq!(decoded.stamp().batch(), stamp.batch());
assert_eq!(decoded.stamp().timestamp(), stamp.timestamp());
assert_eq!(decoded, stamped);
}
#[test]
fn typed_single_owner_round_trip() {
let chunk = single_owner_chunk();
let address = *chunk.address();
let stamp = test_stamp();
let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
let bytes = stamped.to_typed_bytes();
let decoded = DefaultStampedChunk::from_typed_bytes(&address, &bytes).expect("decode");
assert!(decoded.chunk().is_single_owner());
assert_eq!(*decoded.address(), address);
assert_eq!(decoded.stamp().batch(), stamp.batch());
assert_eq!(decoded.stamp().timestamp(), stamp.timestamp());
assert_eq!(decoded, stamped);
}
#[test]
fn reconstruct_round_trips_from_wire() {
let chunk = content_chunk();
let address = *chunk.address();
let data = Bytes::from(chunk);
let stamp = test_stamp();
let rebuilt = DefaultStampedChunk::reconstruct(address, data.clone(), stamp.clone())
.expect("rebuild");
assert!(rebuilt.chunk().is_content());
assert_eq!(*rebuilt.address(), address);
assert_eq!(rebuilt.stamp(), &stamp);
assert_eq!(rebuilt.into_parts().0.into_bytes(), data);
}
#[test]
fn reconstruct_single_owner_from_wire() {
let chunk = single_owner_chunk();
let address = *chunk.address();
let data = Bytes::from(chunk);
let stamp = test_stamp();
let rebuilt =
DefaultStampedChunk::reconstruct(address, data, stamp.clone()).expect("rebuild");
assert!(rebuilt.chunk().is_single_owner());
assert_eq!(*rebuilt.address(), address);
assert_eq!(rebuilt.stamp(), &stamp);
}
#[test]
fn batch_id_matches_stamp_and_leading_bytes() {
let chunk = content_chunk();
let stamp = test_stamp();
let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
let bytes = stamped.to_typed_bytes();
let id = DefaultStampedChunk::batch_id(&bytes).expect("batch id");
assert_eq!(id, stamp.batch());
assert_eq!(id.as_slice(), &bytes[0..32]);
}
#[test]
fn from_typed_bytes_empty_errors() {
let address: ChunkAddress = [0u8; 32].into();
assert!(DefaultStampedChunk::from_typed_bytes(&address, &[]).is_err());
}
#[test]
fn from_typed_bytes_shorter_than_stamp_errors() {
let address: ChunkAddress = [0u8; 32].into();
let short = [0u8; STAMP_SIZE - 1];
let err = DefaultStampedChunk::from_typed_bytes(&address, &short)
.expect_err("short input must error");
assert!(matches!(err, StampError::InvalidData(_)));
}
#[test]
fn from_typed_bytes_address_mismatch_errors() {
let chunk = content_chunk();
let stamped = DefaultStampedChunk::new(chunk.into(), test_stamp());
let bytes = stamped.to_typed_bytes();
let wrong: ChunkAddress = [0xFFu8; 32].into();
let err = DefaultStampedChunk::from_typed_bytes(&wrong, &bytes)
.expect_err("address mismatch must error");
assert!(matches!(err, StampError::Chunk(_)));
}
#[test]
fn reconstruct_rejects_wrong_address() {
let chunk = content_chunk();
let data = Bytes::from(chunk);
let wrong: ChunkAddress = [0xFFu8; 32].into();
let err = DefaultStampedChunk::reconstruct(wrong, data, test_stamp())
.expect_err("wrong address must error");
assert!(matches!(err, StampError::Chunk(_)));
}
#[test]
fn batch_id_short_errors() {
let short = [0u8; 31];
assert!(DefaultStampedChunk::batch_id(&short).is_err());
}
}