1use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::swarm::errors::Error;
12use crate::swarm::keys::PrivateKey;
13use crate::swarm::typed_bytes::{BatchId, EthAddress, Signature};
14
15pub const NUM_BUCKETS: usize = 1 << 16;
17
18pub const MIN_DEPTH: u8 = 16;
21
22pub const MARSHALED_STAMP_LENGTH: usize = 32 + 8 + 8 + 65;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Envelope {
31 pub batch_id: BatchId,
33 pub index: [u8; 8],
35 pub issuer: EthAddress,
37 pub signature: Signature,
39 pub timestamp: [u8; 8],
41}
42
43#[derive(Clone, Debug)]
50pub struct Stamper {
51 signer: PrivateKey,
52 batch_id: BatchId,
53 buckets: Vec<u32>,
54 depth: u8,
55 max_slot: u32,
56}
57
58impl Stamper {
59 pub fn from_blank(signer: PrivateKey, batch_id: BatchId, depth: u8) -> Result<Self, Error> {
61 Self::from_state(signer, batch_id, vec![0u32; NUM_BUCKETS], depth)
62 }
63
64 pub fn from_state(
67 signer: PrivateKey,
68 batch_id: BatchId,
69 buckets: Vec<u32>,
70 depth: u8,
71 ) -> Result<Self, Error> {
72 if depth <= MIN_DEPTH {
73 return Err(Error::argument(format!(
74 "stamper depth must be > {MIN_DEPTH}, got {depth}"
75 )));
76 }
77 if buckets.len() != NUM_BUCKETS {
78 return Err(Error::argument(format!(
79 "buckets length must be {NUM_BUCKETS}, got {}",
80 buckets.len()
81 )));
82 }
83 let max_slot = 1u32 << (depth - MIN_DEPTH);
84 Ok(Self {
85 signer,
86 batch_id,
87 buckets,
88 depth,
89 max_slot,
90 })
91 }
92
93 pub fn stamp(&mut self, chunk_addr: &[u8]) -> Result<Envelope, Error> {
98 if chunk_addr.len() != 32 {
99 return Err(Error::argument(format!(
100 "chunk address must be 32 bytes, got {}",
101 chunk_addr.len()
102 )));
103 }
104
105 let bucket = u16::from_be_bytes([chunk_addr[0], chunk_addr[1]]) as usize;
106 let height = self.buckets[bucket];
107 if height >= self.max_slot {
108 return Err(Error::argument(format!(
109 "bucket {bucket} is full (height={height}, max_slot={})",
110 self.max_slot
111 )));
112 }
113 self.buckets[bucket] = height + 1;
114
115 let mut index = [0u8; 8];
116 index[..4].copy_from_slice(&(bucket as u32).to_be_bytes());
117 index[4..].copy_from_slice(&height.to_be_bytes());
118
119 let now_ms = SystemTime::now()
120 .duration_since(UNIX_EPOCH)
121 .map(|d| d.as_millis() as u64)
122 .unwrap_or(0);
123 let timestamp = now_ms.to_be_bytes();
124
125 let mut to_sign = Vec::with_capacity(32 + 32 + 8 + 8);
126 to_sign.extend_from_slice(chunk_addr);
127 to_sign.extend_from_slice(self.batch_id.as_bytes());
128 to_sign.extend_from_slice(&index);
129 to_sign.extend_from_slice(×tamp);
130
131 let signature = self.signer.sign(&to_sign)?;
132 let issuer = self.signer.public_key()?.address();
133
134 Ok(Envelope {
135 batch_id: self.batch_id,
136 index,
137 issuer,
138 signature,
139 timestamp,
140 })
141 }
142
143 pub fn state(&self) -> &[u32] {
146 &self.buckets
147 }
148
149 pub fn depth(&self) -> u8 {
151 self.depth
152 }
153
154 pub fn max_slot(&self) -> u32 {
156 self.max_slot
157 }
158
159 pub fn batch_id(&self) -> &BatchId {
161 &self.batch_id
162 }
163}
164
165pub fn marshal_stamp(
171 batch_id: &BatchId,
172 index: &[u8],
173 timestamp: &[u8],
174 signature: &Signature,
175) -> Result<[u8; MARSHALED_STAMP_LENGTH], Error> {
176 if index.len() != 8 {
177 return Err(Error::argument(format!(
178 "invalid index length: {}",
179 index.len()
180 )));
181 }
182 if timestamp.len() != 8 {
183 return Err(Error::argument(format!(
184 "invalid timestamp length: {}",
185 timestamp.len()
186 )));
187 }
188 let mut out = [0u8; MARSHALED_STAMP_LENGTH];
189 out[..32].copy_from_slice(batch_id.as_bytes());
190 out[32..40].copy_from_slice(index);
191 out[40..48].copy_from_slice(timestamp);
192 out[48..].copy_from_slice(signature.as_bytes());
193 Ok(out)
194}
195
196pub fn convert_envelope_to_marshaled_stamp(
202 env: &Envelope,
203) -> Result<[u8; MARSHALED_STAMP_LENGTH], Error> {
204 marshal_stamp(&env.batch_id, &env.index, &env.timestamp, &env.signature)
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 fn signer() -> PrivateKey {
212 PrivateKey::new(&[0x11; 32]).unwrap()
213 }
214
215 fn batch() -> BatchId {
216 BatchId::new(&[0u8; 32]).unwrap()
217 }
218
219 #[test]
220 fn stamp_increments_bucket_and_signs() {
221 let mut stamper = Stamper::from_blank(signer(), batch(), 20).unwrap();
222 let addr = [0u8; 32];
223 let env = stamper.stamp(&addr).unwrap();
224
225 assert_eq!(env.batch_id, batch());
226 assert_eq!(env.signature.as_bytes().len(), 65);
227 assert_eq!(env.index.len(), 8);
228 assert_eq!(env.issuer.as_bytes().len(), 20);
229 assert_eq!(stamper.state()[0], 1);
230
231 let mut to_sign = Vec::new();
233 to_sign.extend_from_slice(&addr);
234 to_sign.extend_from_slice(batch().as_bytes());
235 to_sign.extend_from_slice(&env.index);
236 to_sign.extend_from_slice(&env.timestamp);
237 assert!(env.signature.is_valid(&to_sign, env.issuer));
238
239 let env2 = stamper.stamp(&addr).unwrap();
240 assert_eq!(stamper.state()[0], 2);
241 assert_eq!(&env2.index[4..], &1u32.to_be_bytes());
243 }
244
245 #[test]
246 fn rejects_depth_at_or_below_floor() {
247 assert!(Stamper::from_blank(signer(), batch(), 16).is_err());
248 assert!(Stamper::from_blank(signer(), batch(), 0).is_err());
249 assert!(Stamper::from_blank(signer(), batch(), 17).is_ok());
250 }
251
252 #[test]
253 fn rejects_bad_chunk_address_length() {
254 let mut stamper = Stamper::from_blank(signer(), batch(), 20).unwrap();
255 assert!(stamper.stamp(&[0u8; 31]).is_err());
256 assert!(stamper.stamp(&[0u8; 33]).is_err());
257 }
258
259 #[test]
260 fn bucket_full_errors() {
261 let mut stamper = Stamper::from_blank(signer(), batch(), 17).unwrap();
262 let addr = [0u8; 32];
264 stamper.stamp(&addr).unwrap();
265 stamper.stamp(&addr).unwrap();
266 assert!(stamper.stamp(&addr).is_err());
267 }
268
269 #[test]
270 fn from_state_round_trips() {
271 let mut a = Stamper::from_blank(signer(), batch(), 18).unwrap();
272 a.stamp(&[0u8; 32]).unwrap();
273 a.stamp(&[0u8; 32]).unwrap();
274 let snapshot = a.state().to_vec();
275 let b = Stamper::from_state(signer(), batch(), snapshot, 18).unwrap();
276 assert_eq!(b.state()[0], 2);
277 }
278
279 #[test]
280 fn rejects_wrong_state_length() {
281 assert!(Stamper::from_state(signer(), batch(), vec![0u32; 10], 18).is_err());
282 }
283
284 #[test]
285 fn marshal_stamp_round_trip_matches_layout() {
286 let batch_id = BatchId::new(&[0xaa; 32]).unwrap();
287 let mut stamper = Stamper::from_blank(signer(), batch_id, 17).unwrap();
288 let chunk_addr = [0x42u8; 32];
289 let env = stamper.stamp(&chunk_addr).unwrap();
290
291 let bytes = convert_envelope_to_marshaled_stamp(&env).unwrap();
292 assert_eq!(bytes.len(), MARSHALED_STAMP_LENGTH);
293 assert_eq!(&bytes[..32], batch_id.as_bytes());
294 assert_eq!(&bytes[32..40], &env.index);
295 assert_eq!(&bytes[40..48], &env.timestamp);
296 assert_eq!(&bytes[48..], env.signature.as_bytes());
297
298 let mut ts = [0u8; 8];
300 ts.copy_from_slice(&bytes[40..48]);
301 let ts = u64::from_be_bytes(ts);
302 let now_ms = std::time::SystemTime::now()
303 .duration_since(std::time::UNIX_EPOCH)
304 .unwrap()
305 .as_millis() as u64;
306 assert!(ts <= now_ms);
307 assert!(now_ms - ts < 24 * 60 * 60 * 1000);
308 }
309
310 #[test]
311 fn marshal_stamp_rejects_short_index_or_timestamp() {
312 let batch_id = BatchId::new(&[0u8; 32]).unwrap();
313 let sig = crate::swarm::typed_bytes::Signature::new(&[0xab; 65]).unwrap();
314 assert!(marshal_stamp(&batch_id, &[1, 2, 3], &[0u8; 8], &sig).is_err());
315 assert!(marshal_stamp(&batch_id, &[0u8; 8], &[1, 2], &sig).is_err());
316 assert!(marshal_stamp(&batch_id, &[0u8; 8], &[0u8; 8], &sig).is_ok());
317 }
318
319 #[test]
320 fn bucket_routing_uses_first_two_bytes_be() {
321 let mut stamper = Stamper::from_blank(signer(), batch(), 20).unwrap();
322 let mut addr = [0u8; 32];
323 addr[0] = 0xab;
324 addr[1] = 0xcd;
325 stamper.stamp(&addr).unwrap();
326 assert_eq!(stamper.state()[0xabcd], 1);
327 assert_eq!(stamper.state()[0], 0);
328 }
329}