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> {
357 let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
358 let prehash = digest.to_prehash();
359
360 let msg_hash = eip191_hash_message(prehash.as_slice());
362
363 let k256_sig = self
365 .sig
366 .to_k256()
367 .map_err(|_| StampError::InvalidSignature)?;
368
369 let recovery_id = self.sig.recid();
371
372 VerifyingKey::recover_from_prehash(msg_hash.as_slice(), &k256_sig, recovery_id)
374 .map_err(|_| StampError::InvalidSignature)
375 }
376
377 pub fn verify_with_pubkey(
408 &self,
409 chunk_address: &SwarmAddress,
410 pubkey: &VerifyingKey,
411 ) -> Result<(), StampError> {
412 use alloy_signer::k256::ecdsa::signature::hazmat::PrehashVerifier;
413
414 let digest = StampDigest::new(*chunk_address, self.batch, self.index, self.timestamp);
415 let prehash = digest.to_prehash();
416
417 let msg_hash = eip191_hash_message(prehash.as_slice());
419
420 let k256_sig = self
422 .sig
423 .to_k256()
424 .map_err(|_| StampError::InvalidSignature)?;
425
426 pubkey
428 .verify_prehash(msg_hash.as_slice(), &k256_sig)
429 .map_err(|_| StampError::InvalidSignature)
430 }
431}
432
433#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub struct StampDigest {
438 pub chunk_address: SwarmAddress,
440 pub batch_id: BatchId,
442 pub index: StampIndex,
444 pub timestamp: u64,
446}
447
448impl StampDigest {
449 #[inline]
451 pub const fn new(
452 chunk_address: SwarmAddress,
453 batch_id: BatchId,
454 index: StampIndex,
455 timestamp: u64,
456 ) -> Self {
457 Self {
458 chunk_address,
459 batch_id,
460 index,
461 timestamp,
462 }
463 }
464
465 pub fn to_prehash(&self) -> B256 {
469 use alloy_primitives::keccak256;
470
471 let mut data = [0u8; 32 + 32 + 8 + 8]; data[..32].copy_from_slice(self.chunk_address.as_bytes());
473 data[32..64].copy_from_slice(self.batch_id.as_slice());
474 data[64..72].copy_from_slice(&self.index.to_be_bytes());
475 data[72..80].copy_from_slice(&self.timestamp.to_be_bytes());
476
477 keccak256(data)
478 }
479}
480
481impl From<Stamp> for StampBytes {
482 #[inline]
483 fn from(stamp: Stamp) -> Self {
484 stamp.to_bytes()
485 }
486}
487
488impl TryFrom<StampBytes> for Stamp {
489 type Error = StampError;
490
491 #[inline]
492 fn try_from(bytes: StampBytes) -> Result<Self, Self::Error> {
493 Self::from_bytes(&bytes)
494 }
495}
496
497#[cfg(feature = "arbitrary")]
500impl<'a> arbitrary::Arbitrary<'a> for StampIndex {
501 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
502 Ok(Self::new(u.arbitrary()?, u.arbitrary()?))
503 }
504}
505
506#[cfg(feature = "arbitrary")]
507impl<'a> arbitrary::Arbitrary<'a> for Stamp {
508 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
509 use alloy_primitives::U256;
510
511 let batch: B256 = u.arbitrary()?;
512 let index = StampIndex::arbitrary(u)?;
513 let timestamp: u64 = u.arbitrary()?;
514
515 let r = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
517 let s = U256::from_be_bytes(u.arbitrary::<[u8; 32]>()?);
518 let v: bool = u.arbitrary()?;
519 let sig = Signature::new(r, s, v);
520
521 Ok(Self::with_index(batch, index, timestamp, sig))
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use alloy_primitives::hex;
529
530 const TEST_BATCH_ID: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d36761369";
531 const TEST_STAMP: &str = "c3387832bb1b88acbcd0ffdb65a08ef077d98c08d4bee576a72dbe3d367613690000cbe5000000000000018921ff0dbb29169df9e6364e26c6ca6b17745c10b9d6a36ea38e204f2e3cc64a8373c0661f5bb0a347c61d8d1689b0dcf8354117686a6a18d08cff927f526de5fc61b2b7491b";
532
533 #[test]
534 fn test_stamp_index_encode_decode() {
535 let idx = StampIndex::new(0x1234, 0x5678);
536 assert_eq!(idx.encode(), 0x0000123400005678);
537
538 let decoded = StampIndex::decode(0x0000123400005678);
539 assert_eq!(decoded, idx);
540 }
541
542 #[test]
543 fn test_stamp_index_bytes() {
544 let idx = StampIndex::new(0x1234, 0x5678);
545 let bytes = idx.to_be_bytes();
546 let restored = StampIndex::from_be_bytes(bytes);
547 assert_eq!(idx, restored);
548 }
549
550 #[test]
551 fn test_stamp_index_conversions() {
552 let idx = StampIndex::new(100, 50);
553 let tuple: (u32, u32) = idx.into();
554 assert_eq!(tuple, (100, 50));
555
556 let back: StampIndex = tuple.into();
557 assert_eq!(back, idx);
558 }
559
560 #[test]
561 fn test_stamp_roundtrip() {
562 let batch = B256::ZERO;
563 let sig = Signature::test_signature();
564 let stamp = Stamp::new(batch, 100, 50, 1234567890, sig);
565
566 let bytes = stamp.to_bytes();
567 let restored = Stamp::from_bytes(&bytes).unwrap();
568
569 assert_eq!(stamp, restored);
570 }
571
572 #[test]
573 fn test_stamp_from_known_data() {
574 let bytes = hex::decode(TEST_STAMP).unwrap();
575 let stamp = Stamp::try_from_slice(&bytes).unwrap();
576
577 let expected_batch = B256::from_slice(&hex::decode(TEST_BATCH_ID).unwrap());
578 assert_eq!(stamp.batch(), expected_batch);
579 assert_eq!(stamp.bucket(), 52197); assert_eq!(stamp.index(), 0);
581 assert_eq!(stamp.timestamp(), 1688492510651);
582 }
583
584 #[test]
585 fn test_stamp_with_index() {
586 let batch = B256::ZERO;
587 let idx = StampIndex::new(100, 50);
588 let sig = Signature::test_signature();
589 let stamp = Stamp::with_index(batch, idx, 1234567890, sig);
590
591 assert_eq!(stamp.stamp_index(), idx);
592 assert_eq!(stamp.bucket(), 100);
593 assert_eq!(stamp.index(), 50);
594 }
595
596 #[test]
597 fn test_stamp_size() {
598 assert_eq!(STAMP_SIZE, 113);
599 }
600
601 #[test]
602 fn test_invalid_slice_size() {
603 let bytes = [0u8; 100];
604 let result = Stamp::try_from_slice(&bytes);
605 assert!(matches!(result, Err(StampError::InvalidData(_))));
606 }
607
608 #[test]
609 fn test_from_conversions() {
610 let sig = Signature::test_signature();
611 let stamp = Stamp::new(B256::ZERO, 1, 2, 3, sig);
612
613 let bytes: StampBytes = stamp.clone().into();
615 let back: Stamp = bytes.try_into().unwrap();
617 assert_eq!(stamp, back);
618 }
619
620 #[test]
625 fn test_recover_signer() {
626 let chunk_addr_bytes =
628 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
629 .unwrap();
630 let full_stamp_bytes = hex::decode(
631 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
632 ).unwrap();
633 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
634
635 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
636 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
637
638 let recovered = stamp.recover_signer(&chunk_address).unwrap();
640 assert_eq!(recovered, expected_owner);
641 }
642
643 #[test]
645 fn test_verify() {
646 let chunk_addr_bytes =
648 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
649 .unwrap();
650 let full_stamp_bytes = hex::decode(
651 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
652 ).unwrap();
653 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
654 let wrong_owner: Address = "0000000000000000000000000000000000000001".parse().unwrap();
655
656 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
657 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
658
659 assert!(stamp.verify(&chunk_address, expected_owner).is_ok());
661
662 let result = stamp.verify(&chunk_address, wrong_owner);
664 assert!(matches!(result, Err(StampError::OwnerMismatch { .. })));
665 }
666
667 #[test]
669 fn test_recover_pubkey() {
670 use alloy_signer::utils::public_key_to_address;
671
672 let chunk_addr_bytes =
674 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
675 .unwrap();
676 let full_stamp_bytes = hex::decode(
677 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
678 ).unwrap();
679 let expected_owner: Address = "8d3766440f0d7b949a5e32995d09619a7f86e632".parse().unwrap();
680
681 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
682 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
683
684 let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
686
687 let recovered_addr = public_key_to_address(&pubkey);
689 assert_eq!(recovered_addr, expected_owner);
690 }
691
692 #[test]
694 fn test_verify_with_pubkey() {
695 let chunk_addr_bytes =
697 hex::decode("0000000000000000000000000000000000000000000000000000000000000002")
698 .unwrap();
699 let full_stamp_bytes = hex::decode(
700 "000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000003496cb9ac06221d39c3f6a7dd3b9c2301c1f923162b90d5443e42023f34ff908945b0da1c297190f111b7c6ebc828648ead8f7fce06c0364cb5a833410230c5c01c"
701 ).unwrap();
702
703 let chunk_address = SwarmAddress::new(chunk_addr_bytes.try_into().unwrap());
704 let stamp = Stamp::try_from_slice(&full_stamp_bytes).unwrap();
705
706 let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
708
709 let result = stamp.verify_with_pubkey(&chunk_address, &pubkey);
711 assert!(result.is_ok());
712 }
713
714 #[test]
716 fn test_verify_with_wrong_pubkey() {
717 use alloy_signer::SignerSync;
718 use alloy_signer_local::PrivateKeySigner;
719
720 let signer = PrivateKeySigner::random();
722 let chunk_address = SwarmAddress::new([0xAB; 32]);
723 let batch_id = B256::ZERO;
724 let index = StampIndex::new(0, 0);
725 let timestamp = 12345u64;
726
727 let digest = StampDigest::new(chunk_address, batch_id, index, timestamp);
728 let prehash = digest.to_prehash();
729
730 let sig = signer.sign_message_sync(prehash.as_slice()).unwrap();
732 let stamp = Stamp::with_index(batch_id, index, timestamp, sig);
733
734 let correct_pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
736
737 let wrong_signer = PrivateKeySigner::random();
739 let wrong_pubkey = wrong_signer.credential().verifying_key();
740
741 assert!(
743 stamp
744 .verify_with_pubkey(&chunk_address, &correct_pubkey)
745 .is_ok()
746 );
747
748 assert!(
750 stamp
751 .verify_with_pubkey(&chunk_address, wrong_pubkey)
752 .is_err()
753 );
754 }
755}