nectar_postage/
stamped.rs1use alloc::vec::Vec;
14
15use nectar_primitives::{AnyChunk, ChunkAddress, DEFAULT_BODY_SIZE, bytes::Bytes};
16
17use crate::{BatchId, STAMP_SIZE, Stamp, StampError};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct StampedChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
38 chunk: AnyChunk<BODY_SIZE>,
39 stamp: Stamp,
40}
41
42impl<const BODY_SIZE: usize> StampedChunk<BODY_SIZE> {
43 #[inline]
45 #[must_use]
46 pub const fn new(chunk: AnyChunk<BODY_SIZE>, stamp: Stamp) -> Self {
47 Self { chunk, stamp }
48 }
49
50 #[inline]
52 #[must_use]
53 pub const fn chunk(&self) -> &AnyChunk<BODY_SIZE> {
54 &self.chunk
55 }
56
57 #[inline]
59 #[must_use]
60 pub const fn stamp(&self) -> &Stamp {
61 &self.stamp
62 }
63
64 #[inline]
66 #[must_use]
67 pub fn address(&self) -> &ChunkAddress {
68 self.chunk.address()
69 }
70
71 #[inline]
73 #[must_use]
74 pub fn into_parts(self) -> (AnyChunk<BODY_SIZE>, Stamp) {
75 (self.chunk, self.stamp)
76 }
77
78 #[must_use]
87 pub fn to_typed_bytes(&self) -> Vec<u8> {
88 let stamp = self.stamp.to_bytes();
89 let chunk = self.chunk.to_typed_bytes();
90 let mut out = Vec::with_capacity(stamp.len() + chunk.len());
91 out.extend_from_slice(&stamp);
92 out.extend_from_slice(&chunk);
93 out
94 }
95
96 pub fn from_typed_bytes(address: &ChunkAddress, bytes: &[u8]) -> Result<Self, StampError> {
108 if bytes.len() < STAMP_SIZE {
109 return Err(StampError::InvalidData(
110 "stamped chunk shorter than a stamp",
111 ));
112 }
113 let (stamp_bytes, chunk_bytes) = bytes.split_at(STAMP_SIZE);
114 let stamp = Stamp::try_from_slice(stamp_bytes)?;
115 let chunk = AnyChunk::<BODY_SIZE>::from_typed_bytes(address, chunk_bytes)
116 .map_err(|_| StampError::Chunk("failed to decode typed chunk"))?;
117 Ok(Self::new(chunk, stamp))
118 }
119
120 pub fn reconstruct(
132 expected: ChunkAddress,
133 data: Bytes,
134 stamp: Stamp,
135 ) -> Result<Self, StampError> {
136 let chunk = AnyChunk::<BODY_SIZE>::from_wire_bytes(&expected, data)
137 .map_err(|_| StampError::Chunk("chunk bytes do not match expected address"))?;
138 Ok(Self::new(chunk, stamp))
139 }
140
141 pub fn batch_id(typed_bytes: &[u8]) -> Result<BatchId, StampError> {
153 let id = typed_bytes.get(..32).ok_or(StampError::InvalidData(
154 "typed bytes shorter than a batch id",
155 ))?;
156 Ok(BatchId::from_slice(id))
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use alloy_primitives::{B256, Signature};
163 use alloy_signer_local::PrivateKeySigner;
164 use nectar_primitives::{Chunk, ContentChunk, SingleOwnerChunk, bytes::Bytes};
165
166 use super::*;
167
168 type DefaultStampedChunk = StampedChunk<DEFAULT_BODY_SIZE>;
169
170 fn test_stamp() -> Stamp {
171 let sig = Signature::from_raw(&[1u8; 65]).expect("valid signature");
172 Stamp::new(B256::repeat_byte(0xaa), 3, 7, 42, sig)
173 }
174
175 fn content_chunk() -> ContentChunk<DEFAULT_BODY_SIZE> {
176 ContentChunk::new(&b"hello swarm"[..]).expect("valid content chunk")
177 }
178
179 fn single_owner_chunk() -> SingleOwnerChunk<DEFAULT_BODY_SIZE> {
180 let signer = PrivateKeySigner::from_bytes(&B256::repeat_byte(0x11)).expect("valid signer");
181 SingleOwnerChunk::new(B256::repeat_byte(0x22), &b"soc payload"[..], &signer)
182 .expect("valid soc")
183 }
184
185 #[test]
186 fn into_parts_round_trips_the_fields() {
187 let chunk: AnyChunk = content_chunk().into();
188 let stamp = test_stamp();
189 let stamped = DefaultStampedChunk::new(chunk.clone(), stamp.clone());
190 assert_eq!(stamped.address(), chunk.address());
191 let (got_chunk, got_stamp) = stamped.into_parts();
192 assert_eq!(got_chunk, chunk);
193 assert_eq!(got_stamp, stamp);
194 }
195
196 #[test]
197 fn typed_content_round_trip() {
198 let chunk = content_chunk();
199 let address = *chunk.address();
200 let stamp = test_stamp();
201 let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
202
203 let bytes = stamped.to_typed_bytes();
204 let decoded = DefaultStampedChunk::from_typed_bytes(&address, &bytes).expect("decode");
205
206 assert!(decoded.chunk().is_content());
207 assert_eq!(*decoded.address(), address);
208 assert_eq!(decoded.stamp().batch(), stamp.batch());
209 assert_eq!(decoded.stamp().timestamp(), stamp.timestamp());
210 assert_eq!(decoded, stamped);
211 }
212
213 #[test]
214 fn typed_single_owner_round_trip() {
215 let chunk = single_owner_chunk();
216 let address = *chunk.address();
217 let stamp = test_stamp();
218 let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
219
220 let bytes = stamped.to_typed_bytes();
221 let decoded = DefaultStampedChunk::from_typed_bytes(&address, &bytes).expect("decode");
222
223 assert!(decoded.chunk().is_single_owner());
224 assert_eq!(*decoded.address(), address);
225 assert_eq!(decoded.stamp().batch(), stamp.batch());
226 assert_eq!(decoded.stamp().timestamp(), stamp.timestamp());
227 assert_eq!(decoded, stamped);
228 }
229
230 #[test]
231 fn reconstruct_round_trips_from_wire() {
232 let chunk = content_chunk();
233 let address = *chunk.address();
234 let data = Bytes::from(chunk);
235 let stamp = test_stamp();
236
237 let rebuilt = DefaultStampedChunk::reconstruct(address, data.clone(), stamp.clone())
238 .expect("rebuild");
239 assert!(rebuilt.chunk().is_content());
240 assert_eq!(*rebuilt.address(), address);
241 assert_eq!(rebuilt.stamp(), &stamp);
242 assert_eq!(rebuilt.into_parts().0.into_bytes(), data);
243 }
244
245 #[test]
246 fn reconstruct_single_owner_from_wire() {
247 let chunk = single_owner_chunk();
248 let address = *chunk.address();
249 let data = Bytes::from(chunk);
250 let stamp = test_stamp();
251
252 let rebuilt =
253 DefaultStampedChunk::reconstruct(address, data, stamp.clone()).expect("rebuild");
254 assert!(rebuilt.chunk().is_single_owner());
255 assert_eq!(*rebuilt.address(), address);
256 assert_eq!(rebuilt.stamp(), &stamp);
257 }
258
259 #[test]
260 fn batch_id_matches_stamp_and_leading_bytes() {
261 let chunk = content_chunk();
262 let stamp = test_stamp();
263 let stamped = DefaultStampedChunk::new(chunk.into(), stamp.clone());
264 let bytes = stamped.to_typed_bytes();
265
266 let id = DefaultStampedChunk::batch_id(&bytes).expect("batch id");
267 assert_eq!(id, stamp.batch());
268 assert_eq!(id.as_slice(), &bytes[0..32]);
269 }
270
271 #[test]
272 fn from_typed_bytes_empty_errors() {
273 let address: ChunkAddress = [0u8; 32].into();
274 assert!(DefaultStampedChunk::from_typed_bytes(&address, &[]).is_err());
275 }
276
277 #[test]
278 fn from_typed_bytes_shorter_than_stamp_errors() {
279 let address: ChunkAddress = [0u8; 32].into();
280 let short = [0u8; STAMP_SIZE - 1];
281 let err = DefaultStampedChunk::from_typed_bytes(&address, &short)
282 .expect_err("short input must error");
283 assert!(matches!(err, StampError::InvalidData(_)));
284 }
285
286 #[test]
287 fn from_typed_bytes_address_mismatch_errors() {
288 let chunk = content_chunk();
289 let stamped = DefaultStampedChunk::new(chunk.into(), test_stamp());
290 let bytes = stamped.to_typed_bytes();
291
292 let wrong: ChunkAddress = [0xFFu8; 32].into();
293 let err = DefaultStampedChunk::from_typed_bytes(&wrong, &bytes)
294 .expect_err("address mismatch must error");
295 assert!(matches!(err, StampError::Chunk(_)));
296 }
297
298 #[test]
299 fn reconstruct_rejects_wrong_address() {
300 let chunk = content_chunk();
301 let data = Bytes::from(chunk);
302 let wrong: ChunkAddress = [0xFFu8; 32].into();
303 let err = DefaultStampedChunk::reconstruct(wrong, data, test_stamp())
304 .expect_err("wrong address must error");
305 assert!(matches!(err, StampError::Chunk(_)));
306 }
307
308 #[test]
309 fn batch_id_short_errors() {
310 let short = [0u8; 31];
311 assert!(DefaultStampedChunk::batch_id(&short).is_err());
312 }
313}