1use alloy_primitives::{Address, B256, Signature, eip191_hash_message};
4use alloy_signer::k256::ecdsa::VerifyingKey;
5use byteorder::{BigEndian, ByteOrder};
6use nectar_primitives::SwarmAddress;
7
8use crate::{BatchId, StampError};
9
10pub const STAMP_SIZE: usize = 113;
14
15pub type StampBytes = [u8; STAMP_SIZE];
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct StampIndex {
33 bucket: u32,
38 index: u32,
42}
43
44impl StampIndex {
45 #[inline]
47 pub const fn new(bucket: u32, index: u32) -> Self {
48 Self { bucket, index }
49 }
50
51 #[inline]
53 pub const fn bucket(&self) -> u32 {
54 self.bucket
55 }
56
57 #[inline]
59 pub const fn index(&self) -> u32 {
60 self.index
61 }
62
63 #[inline]
80 pub const fn encode(&self) -> u64 {
81 ((self.bucket as u64) << 32) | (self.index as u64)
82 }
83
84 #[inline]
88 pub const fn decode(encoded: u64) -> Self {
89 Self {
90 bucket: (encoded >> 32) as u32,
91 index: encoded as u32,
92 }
93 }
94
95 #[inline]
97 pub const fn to_be_bytes(&self) -> [u8; 8] {
98 self.encode().to_be_bytes()
99 }
100
101 #[inline]
103 pub const fn from_be_bytes(bytes: [u8; 8]) -> Self {
104 Self::decode(u64::from_be_bytes(bytes))
105 }
106}
107
108impl From<(u32, u32)> for StampIndex {
109 fn from((bucket, index): (u32, u32)) -> Self {
110 Self::new(bucket, index)
111 }
112}
113
114impl From<StampIndex> for (u32, u32) {
115 fn from(idx: StampIndex) -> Self {
116 (idx.bucket, idx.index)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135pub struct Stamp {
136 batch: BatchId,
138 index: StampIndex,
140 timestamp: u64,
142 sig: Signature,
144}
145
146impl Stamp {
147 #[inline]
149 pub const fn new(
150 batch: BatchId,
151 bucket: u32,
152 index: u32,
153 timestamp: u64,
154 sig: Signature,
155 ) -> Self {
156 Self {
157 batch,
158 index: StampIndex::new(bucket, index),
159 timestamp,
160 sig,
161 }
162 }
163
164 #[inline]
166 pub const fn with_index(
167 batch: BatchId,
168 index: StampIndex,
169 timestamp: u64,
170 sig: Signature,
171 ) -> Self {
172 Self {
173 batch,
174 index,
175 timestamp,
176 sig,
177 }
178 }
179
180 #[inline]
182 pub const fn batch(&self) -> BatchId {
183 self.batch
184 }
185
186 #[inline]
188 pub const fn stamp_index(&self) -> StampIndex {
189 self.index
190 }
191
192 #[inline]
194 pub const fn bucket(&self) -> u32 {
195 self.index.bucket()
196 }
197
198 #[inline]
200 pub const fn index(&self) -> u32 {
201 self.index.index()
202 }
203
204 #[inline]
206 pub const fn timestamp(&self) -> u64 {
207 self.timestamp
208 }
209
210 #[inline]
212 pub const fn signature(&self) -> &Signature {
213 &self.sig
214 }
215
216 #[inline]
218 pub fn to_bytes(&self) -> StampBytes {
219 let mut bytes = [0u8; STAMP_SIZE];
220 bytes[..32].copy_from_slice(self.batch.as_slice());
221 BigEndian::write_u32(&mut bytes[32..36], self.index.bucket());
222 BigEndian::write_u32(&mut bytes[36..40], self.index.index());
223 BigEndian::write_u64(&mut bytes[40..48], self.timestamp);
224 bytes[48..STAMP_SIZE].copy_from_slice(&self.sig.as_bytes());
225 bytes
226 }
227
228 #[inline]
232 pub fn from_bytes(bytes: &StampBytes) -> Result<Self, StampError> {
233 let batch = B256::from_slice(&bytes[..32]);
234 let bucket = BigEndian::read_u32(&bytes[32..36]);
235 let index = BigEndian::read_u32(&bytes[36..40]);
236 let timestamp = BigEndian::read_u64(&bytes[40..48]);
237
238 let sig = Signature::from_raw(&bytes[48..STAMP_SIZE])
239 .map_err(|_| StampError::InvalidSignature)?;
240
241 Ok(Self {
242 batch,
243 index: StampIndex::new(bucket, index),
244 timestamp,
245 sig,
246 })
247 }
248
249 #[inline]
253 pub fn try_from_slice(bytes: &[u8]) -> Result<Self, StampError> {
254 if bytes.len() != STAMP_SIZE {
255 return Err(StampError::InvalidData("stamp must be exactly 113 bytes"));
256 }
257
258 let mut stamp_bytes = [0u8; STAMP_SIZE];
260 stamp_bytes.copy_from_slice(bytes);
261 Self::from_bytes(&stamp_bytes)
262 }
263
264 pub fn recover_signer(&self, chunk_address: &SwarmAddress) -> Result<Address, StampError> {
285 let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
286 let prehash = digest.to_prehash();
287
288 self.sig
290 .recover_address_from_msg(prehash.as_slice())
291 .map_err(|_| StampError::InvalidSignature)
292 }
293
294 pub fn verify(&self, chunk_address: &SwarmAddress, owner: Address) -> Result<(), StampError> {
316 let recovered = self.recover_signer(chunk_address)?;
317 if recovered != owner {
318 return Err(StampError::OwnerMismatch {
319 expected: owner,
320 actual: recovered,
321 });
322 }
323 Ok(())
324 }
325
326 pub fn recover_pubkey(&self, chunk_address: &SwarmAddress) -> Result<VerifyingKey, StampError> {
353 let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
354 let prehash = digest.to_prehash();
355
356 let msg_hash = eip191_hash_message(prehash.as_slice());
358
359 let k256_sig = self
361 .sig
362 .to_k256()
363 .map_err(|_| StampError::InvalidSignature)?;
364
365 let recovery_id = self.sig.recid();
367
368 VerifyingKey::recover_from_prehash(msg_hash.as_slice(), &k256_sig, recovery_id)
370 .map_err(|_| StampError::InvalidSignature)
371 }
372
373 pub fn verify_with_pubkey(
404 &self,
405 chunk_address: &SwarmAddress,
406 pubkey: &VerifyingKey,
407 ) -> Result<(), StampError> {
408 use alloy_signer::k256::ecdsa::signature::hazmat::PrehashVerifier;
409
410 let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
411 let prehash = digest.to_prehash();
412
413 let msg_hash = eip191_hash_message(prehash.as_slice());
415
416 let k256_sig = self
418 .sig
419 .to_k256()
420 .map_err(|_| StampError::InvalidSignature)?;
421
422 pubkey
424 .verify_prehash(msg_hash.as_slice(), &k256_sig)
425 .map_err(|_| StampError::InvalidSignature)
426 }
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub struct StampDigest {
434 pub chunk_address: SwarmAddress,
436 pub batch_id: BatchId,
438 pub index: StampIndex,
440 pub timestamp: u64,
442}
443
444impl StampDigest {
445 #[inline]
447 pub const fn new(
448 chunk_address: SwarmAddress,
449 batch_id: BatchId,
450 index: StampIndex,
451 timestamp: u64,
452 ) -> Self {
453 Self {
454 chunk_address,
455 batch_id,
456 index,
457 timestamp,
458 }
459 }
460
461 pub fn to_prehash(&self) -> B256 {
465 use alloy_primitives::keccak256;
466
467 let mut data = [0u8; 32 + 32 + 8 + 8]; data[..32].copy_from_slice(self.chunk_address.as_bytes());
469 data[32..64].copy_from_slice(self.batch_id.as_slice());
470 data[64..72].copy_from_slice(&self.index.to_be_bytes());
471 data[72..80].copy_from_slice(&self.timestamp.to_be_bytes());
472
473 keccak256(data)
474 }
475}
476
477impl From<Stamp> for StampBytes {
478 #[inline]
479 fn from(stamp: Stamp) -> Self {
480 stamp.to_bytes()
481 }
482}
483
484impl TryFrom<StampBytes> for Stamp {
485 type Error = StampError;
486
487 #[inline]
488 fn try_from(bytes: StampBytes) -> Result<Self, Self::Error> {
489 Self::from_bytes(&bytes)
490 }
491}
492
493#[cfg(feature = "arbitrary")]
496impl<'a> arbitrary::Arbitrary<'a> for StampIndex {
497 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
498 Ok(Self::new(u.arbitrary()?, u.arbitrary()?))
499 }
500}
501
502#[cfg(feature = "arbitrary")]
503impl<'a> arbitrary::Arbitrary<'a> for Stamp {
504 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
505 use alloy_primitives::U256;
506
507 let batch: B256 = u.arbitrary()?;
508 let index = StampIndex::arbitrary(u)?;
509 let timestamp: u64 = u.arbitrary()?;
510
511 let r = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
513 let s = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
514 let v: bool = u.arbitrary()?;
515 let sig = Signature::new(r, s, v);
516
517 Ok(Self::with_index(batch, index, timestamp, sig))
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use alloy_primitives::hex;
525
526 const TEST_BATCH_ID: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d36761369";
527 const TEST_STAMP: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d367613690000cbe5000000000000018921ff0dbb29169df9e6364e26c6ca6b17745c10b9d6a36ea38e204f2e3cc64a8373c0661f5bb0a347c61d8d1689b0dcf8354117686a6a18d08cff927f526de5fc61b2b7491b";
528
529 #[test]
530 fn test_stamp_index_encode_decode() {
531 let idx = StampIndex::new(0x1234, 0x5678);
532 assert_eq!(idx.encode(), 0x0000123400005678);
533
534 let decoded = StampIndex::decode(0x0000123400005678);
535 assert_eq!(decoded, idx);
536 }
537
538 #[test]
539 fn test_stamp_index_bytes() {
540 let idx = StampIndex::new(0x1234, 0x5678);
541 let bytes = idx.to_be_bytes();
542 let restored = StampIndex::from_be_bytes(bytes);
543 assert_eq!(idx, restored);
544 }
545
546 #[test]
547 fn test_stamp_index_conversions() {
548 let idx = StampIndex::new(100, 50);
549 let tuple: (u32, u32) = idx.into();
550 assert_eq!(tuple, (100, 50));
551
552 let back: StampIndex = tuple.into();
553 assert_eq!(back, idx);
554 }
555
556 #[test]
557 fn test_stamp_roundtrip() {
558 let batch = B256::ZERO;
559 let sig = Signature::test_signature();
560 let stamp = Stamp::new(batch, 100, 50, 1234567890, sig);
561
562 let bytes = stamp.to_bytes();
563 let restored = Stamp::from_bytes(&bytes).unwrap();
564
565 assert_eq!(stamp, restored);
566 }
567
568 #[test]
569 fn test_stamp_from_known_data() {
570 let bytes = hex::decode(TEST_STAMP).unwrap();
571 let stamp = Stamp::try_from_slice(&bytes).unwrap();
572
573 let expected_batch = B256::from_slice(&hex::decode(TEST_BATCH_ID).unwrap());
574 assert_eq!(stamp.batch(), expected_batch);
575 assert_eq!(stamp.bucket(), 52197); assert_eq!(stamp.index(), 0);
577 assert_eq!(stamp.timestamp(), 1688492510651);
578 }
579
580 #[test]
581 fn test_stamp_with_index() {
582 let batch = B256::ZERO;
583 let idx = StampIndex::new(100, 50);
584 let sig = Signature::test_signature();
585 let stamp = Stamp::with_index(batch, idx, 1234567890, sig);
586
587 assert_eq!(stamp.stamp_index(), idx);
588 assert_eq!(stamp.bucket(), 100);
589 assert_eq!(stamp.index(), 50);
590 }
591
592 #[test]
593 fn test_stamp_size() {
594 assert_eq!(STAMP_SIZE, 113);
595 }
596
597 #[test]
598 fn test_invalid_slice_size() {
599 let bytes = [0u8; 100];
600 let result = Stamp::try_from_slice(&bytes);
601 assert!(matches!(result, Err(StampError::InvalidData(_))));
602 }
603
604 #[test]
605 fn test_from_conversions() {
606 let sig = Signature::test_signature();
607 let stamp = Stamp::new(B256::ZERO, 1, 2, 3, sig);
608
609 let bytes: StampBytes = stamp.clone().into();
611 let back: Stamp = bytes.try_into().unwrap();
613 assert_eq!(stamp, back);
614 }
615
616 #[test]
621 fn test_recover_signer() {
622 let chunk_addr_bytes =
624 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
625 .unwrap();
626 let full_stamp_bytes = hex::decode(
627 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
628 ).unwrap();
629 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
630
631 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
632 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
633
634 let recovered = stamp.recover_signer(&chunk_address).unwrap();
636 assert_eq!(recovered, expected_owner);
637 }
638
639 #[test]
641 fn test_verify() {
642 let chunk_addr_bytes =
644 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
645 .unwrap();
646 let full_stamp_bytes = hex::decode(
647 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
648 ).unwrap();
649 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
650 let wrong_owner: Address = "0000000000000000000000000000000000000001".parse().unwrap();
651
652 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
653 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
654
655 assert!(stamp.verify(&chunk_address, expected_owner).is_ok());
657
658 let result = stamp.verify(&chunk_address, wrong_owner);
660 assert!(matches!(result, Err(StampError::OwnerMismatch { .. })));
661 }
662
663 #[test]
665 fn test_recover_pubkey() {
666 use alloy_signer::utils::public_key_to_address;
667
668 let chunk_addr_bytes =
670 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
671 .unwrap();
672 let full_stamp_bytes = hex::decode(
673 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
674 ).unwrap();
675 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
676
677 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
678 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
679
680 let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
682
683 let recovered_addr = public_key_to_address(&pubkey);
685 assert_eq!(recovered_addr, expected_owner);
686 }
687
688 #[test]
690 fn test_verify_with_pubkey() {
691 let chunk_addr_bytes =
693 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
694 .unwrap();
695 let full_stamp_bytes = hex::decode(
696 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
697 ).unwrap();
698
699 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
700 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
701
702 let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
704
705 let result = stamp.verify_with_pubkey(&chunk_address, &pubkey);
707 assert!(result.is_ok());
708 }
709
710 #[test]
712 fn test_verify_with_wrong_pubkey() {
713 use alloy_signer::SignerSync;
714 use alloy_signer_local::PrivateKeySigner;
715
716 let signer = PrivateKeySigner::random();
718 let chunk_address = SwarmAddress::new([0xAB; 32]);
719 let batch_id = B256::ZERO;
720 let index = StampIndex::new(0, 0);
721 let timestamp = 12345u64;
722
723 let digest = StampDigest::new(chunk_address, batch_id, index, timestamp);
724 let prehash = digest.to_prehash();
725
726 let sig = signer.sign_message_sync(prehash.as_slice()).unwrap();
728 let stamp = Stamp::with_index(batch_id, index, timestamp, sig);
729
730 let correct_pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
732
733 let wrong_signer = PrivateKeySigner::random();
735 let wrong_pubkey = wrong_signer.credential().verifying_key();
736
737 assert!(
739 stamp
740 .verify_with_pubkey(&chunk_address, &correct_pubkey)
741 .is_ok()
742 );
743
744 assert!(
746 stamp
747 .verify_with_pubkey(&chunk_address, wrong_pubkey)
748 .is_err()
749 );
750 }
751}