Skip to main content

nectar_postage/
stamp.rs

1//! Postage stamp types.
2
3use 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
10/// The size of a serialized stamp in bytes.
11///
12/// Layout: batch_id (32) + bucket (4) + index (4) + timestamp (8) + signature (65) = 113 bytes
13pub const STAMP_SIZE: usize = 113;
14
15/// A serialized postage stamp as a fixed-size byte array.
16pub type StampBytes = [u8; STAMP_SIZE];
17
18/// A stamp index representing the position of a chunk within a batch.
19///
20/// The stamp index consists of two components:
21/// - `bucket`: The collision bucket determined by the chunk's address (also called "x")
22/// - `index`: The position within that bucket (also called "y")
23///
24/// # Implementation Note
25///
26/// The exact encoding of the stamp index into a single value is **implementation-specific**
27/// and **not defined by the Swarm specifications**. This implementation encodes the index
28/// as a 64-bit value by concatenating the bucket (high 32 bits) and position (low 32 bits)
29/// in big-endian format. Other implementations may use different encodings.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct StampIndex {
33    /// The collision bucket (x coordinate).
34    ///
35    /// Determined by the leading bits of the chunk address, specifically
36    /// the first `bucket_depth` bits interpreted as a big-endian integer.
37    bucket: u32,
38    /// The position within the bucket (y coordinate).
39    ///
40    /// Assigned sequentially as chunks are added to the bucket, starting from 0.
41    index: u32,
42}
43
44impl StampIndex {
45    /// Creates a new stamp index.
46    #[inline]
47    pub const fn new(bucket: u32, index: u32) -> Self {
48        Self { bucket, index }
49    }
50
51    /// Returns the collision bucket (x).
52    #[inline]
53    pub const fn bucket(&self) -> u32 {
54        self.bucket
55    }
56
57    /// Returns the position within the bucket (y).
58    #[inline]
59    pub const fn index(&self) -> u32 {
60        self.index
61    }
62
63    /// Encodes the stamp index as a 64-bit value for use in stamp digest calculation.
64    ///
65    /// # Encoding Format
66    ///
67    /// The encoding concatenates bucket (4 bytes BE) and index (4 bytes BE):
68    /// ```text
69    /// | bucket (32 bits) | index (32 bits) |
70    /// |   high 32 bits   |   low 32 bits   |
71    /// ```
72    ///
73    /// # Implementation Note
74    ///
75    /// This encoding is **implementation-specific** and not defined by the Swarm
76    /// specifications. The Swarm protocol only specifies that the stamp contains
77    /// bucket and index values; the exact wire format for the combined index
78    /// used in signature computation is left to implementations.
79    #[inline]
80    pub const fn encode(&self) -> u64 {
81        ((self.bucket as u64) << 32) | (self.index as u64)
82    }
83
84    /// Decodes a stamp index from a 64-bit encoded value.
85    ///
86    /// See [`encode`](Self::encode) for the encoding format.
87    #[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    /// Converts the index to big-endian bytes (8 bytes total).
96    #[inline]
97    pub const fn to_be_bytes(&self) -> [u8; 8] {
98        self.encode().to_be_bytes()
99    }
100
101    /// Creates a stamp index from big-endian bytes.
102    #[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/// A postage stamp represents proof of payment for storing a chunk.
121///
122/// Stamps are created by signing a message containing the chunk address,
123/// batch ID, stamp index, and timestamp with the batch owner's private key.
124///
125/// # Wire Format
126///
127/// A serialized stamp is 113 bytes:
128/// - Batch ID: 32 bytes
129/// - Bucket (x): 4 bytes, big-endian
130/// - Index (y): 4 bytes, big-endian
131/// - Timestamp: 8 bytes, big-endian
132/// - Signature: 65 bytes (r || s || v)
133#[derive(Debug, Clone, PartialEq, Eq)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
135pub struct Stamp {
136    /// The batch ID this stamp belongs to.
137    batch: BatchId,
138    /// The stamp index (bucket and position).
139    index: StampIndex,
140    /// Timestamp when the stamp was created (nanoseconds since epoch).
141    timestamp: u64,
142    /// The signature proving ownership.
143    sig: Signature,
144}
145
146impl Stamp {
147    /// Creates a new stamp with the given parameters.
148    #[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    /// Creates a new stamp from a stamp index.
165    #[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    /// Returns the batch ID.
181    #[inline]
182    pub const fn batch(&self) -> BatchId {
183        self.batch
184    }
185
186    /// Returns the stamp index.
187    #[inline]
188    pub const fn stamp_index(&self) -> StampIndex {
189        self.index
190    }
191
192    /// Returns the collision bucket.
193    #[inline]
194    pub const fn bucket(&self) -> u32 {
195        self.index.bucket()
196    }
197
198    /// Returns the position within the bucket.
199    #[inline]
200    pub const fn index(&self) -> u32 {
201        self.index.index()
202    }
203
204    /// Returns the timestamp.
205    #[inline]
206    pub const fn timestamp(&self) -> u64 {
207        self.timestamp
208    }
209
210    /// Returns the signature.
211    #[inline]
212    pub const fn signature(&self) -> &Signature {
213        &self.sig
214    }
215
216    /// Serializes the stamp to a 113-byte array.
217    #[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    /// Deserializes a stamp from a 113-byte array.
229    ///
230    /// Returns an error if the signature bytes are invalid.
231    #[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    /// Attempts to deserialize a stamp from a byte slice.
250    ///
251    /// Returns an error if the slice is not exactly 113 bytes or if the signature is invalid.
252    #[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        // Safety: we verified the length above
259        let mut stamp_bytes = [0u8; STAMP_SIZE];
260        stamp_bytes.copy_from_slice(bytes);
261        Self::from_bytes(&stamp_bytes)
262    }
263
264    /// Recovers the signer address from this stamp using EIP-191 message recovery.
265    ///
266    /// This computes the stamp digest from the chunk address and stamp fields,
267    /// then recovers the Ethereum address that signed it.
268    ///
269    /// # Arguments
270    ///
271    /// * `chunk_address` - The address of the chunk this stamp is for
272    ///
273    /// # Returns
274    ///
275    /// The Ethereum address of the signer, or an error if recovery fails.
276    ///
277    /// # Example
278    ///
279    /// ```ignore
280    /// let stamp = Stamp::try_from_slice(&bytes)?;
281    /// let signer = stamp.recover_signer(&chunk_address)?;
282    /// println!("Stamp signed by: {}", signer);
283    /// ```
284    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        // Use recover_address_from_msg for EIP-191 compatibility
289        self.sig
290            .recover_address_from_msg(prehash.as_slice())
291            .map_err(|_| StampError::InvalidSignature)
292    }
293
294    /// Verifies this stamp was signed by the expected owner.
295    ///
296    /// This is a convenience method that calls [`recover_signer`](Self::recover_signer)
297    /// and compares the result to the expected owner address.
298    ///
299    /// # Arguments
300    ///
301    /// * `chunk_address` - The address of the chunk this stamp is for
302    /// * `owner` - The expected owner/signer address
303    ///
304    /// # Returns
305    ///
306    /// `Ok(())` if the stamp was signed by the expected owner,
307    /// or an error if signature recovery fails or the signer doesn't match.
308    ///
309    /// # Example
310    ///
311    /// ```ignore
312    /// let stamp = Stamp::try_from_slice(&bytes)?;
313    /// stamp.verify(&chunk_address, batch.owner())?;
314    /// ```
315    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    /// Recovers the public key from this stamp.
327    ///
328    /// This is useful for caching the public key after the first verification
329    /// of a batch. Subsequent stamps from the same batch can then use
330    /// [`verify_with_pubkey`](Self::verify_with_pubkey) which is approximately
331    /// 10x faster than full signature recovery.
332    ///
333    /// # Arguments
334    ///
335    /// * `chunk_address` - The address of the chunk this stamp is for
336    ///
337    /// # Returns
338    ///
339    /// The public key of the signer, or an error if recovery fails.
340    ///
341    /// # Example
342    ///
343    /// ```ignore
344    /// // First stamp: recover public key and cache it
345    /// let pubkey = first_stamp.recover_pubkey(&first_chunk_address)?;
346    ///
347    /// // Subsequent stamps: fast verification with cached pubkey
348    /// for (stamp, addr) in remaining_stamps {
349    ///     stamp.verify_with_pubkey(&addr, &pubkey)?;
350    /// }
351    /// ```
352    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        // Compute EIP-191 message hash
357        let msg_hash = eip191_hash_message(prehash.as_slice());
358
359        // Convert to k256 signature (64-byte r||s)
360        let k256_sig = self
361            .sig
362            .to_k256()
363            .map_err(|_| StampError::InvalidSignature)?;
364
365        // Get recovery id from signature
366        let recovery_id = self.sig.recid();
367
368        // Recover the public key
369        VerifyingKey::recover_from_prehash(msg_hash.as_slice(), &k256_sig, recovery_id)
370            .map_err(|_| StampError::InvalidSignature)
371    }
372
373    /// Verifies this stamp using a known public key.
374    ///
375    /// This is approximately 10x faster than [`verify`](Self::verify) or
376    /// [`recover_signer`](Self::recover_signer) because it avoids the expensive
377    /// ECDSA public key recovery operation.
378    ///
379    /// Use this when you've already recovered the owner's public key from a
380    /// previous stamp in the same batch (via [`recover_pubkey`](Self::recover_pubkey)).
381    ///
382    /// # Arguments
383    ///
384    /// * `chunk_address` - The address of the chunk this stamp is for
385    /// * `pubkey` - The expected signer's public key (cached from previous recovery)
386    ///
387    /// # Returns
388    ///
389    /// `Ok(())` if the signature is valid for the given public key,
390    /// or an error if verification fails.
391    ///
392    /// # Example
393    ///
394    /// ```ignore
395    /// // First stamp: recover and cache the public key
396    /// let pubkey = first_stamp.recover_pubkey(&first_address)?;
397    /// let owner = alloy_signer::utils::public_key_to_address(&pubkey);
398    ///
399    /// // Fast verification for remaining stamps in the same batch
400    /// second_stamp.verify_with_pubkey(&second_address, &pubkey)?;
401    /// third_stamp.verify_with_pubkey(&third_address, &pubkey)?;
402    /// ```
403    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        // Compute EIP-191 message hash
414        let msg_hash = eip191_hash_message(prehash.as_slice());
415
416        // Convert to k256 signature (64-byte r||s)
417        let k256_sig = self
418            .sig
419            .to_k256()
420            .map_err(|_| StampError::InvalidSignature)?;
421
422        // Verify the signature using prehash
423        pubkey
424            .verify_prehash(msg_hash.as_slice(), &k256_sig)
425            .map_err(|_| StampError::InvalidSignature)
426    }
427}
428
429/// The digest that must be signed to create a valid stamp.
430///
431/// The digest is computed as: `keccak256(chunk_address || batch_id || index || timestamp)`
432#[derive(Debug, Clone, Copy, PartialEq, Eq)]
433pub struct StampDigest {
434    /// The chunk address being stamped.
435    pub chunk_address: SwarmAddress,
436    /// The batch ID.
437    pub batch_id: BatchId,
438    /// The stamp index (bucket and position).
439    pub index: StampIndex,
440    /// The timestamp.
441    pub timestamp: u64,
442}
443
444impl StampDigest {
445    /// Creates a new stamp digest.
446    #[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    /// Computes the 32-byte hash that must be signed.
462    ///
463    /// Format: `keccak256(chunk_address || batch_id || index_bytes || timestamp_bytes)`
464    pub fn to_prehash(&self) -> B256 {
465        use alloy_primitives::keccak256;
466
467        let mut data = [0u8; 32 + 32 + 8 + 8]; // 80 bytes
468        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// Arbitrary implementations for property-based testing
494
495#[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        // Generate a valid signature (r, s must be non-zero for a valid ECDSA signature)
512        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); // 0x0000cbe5
576        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        // From<Stamp> for StampBytes
610        let bytes: StampBytes = stamp.clone().into();
611        // TryFrom<StampBytes> for Stamp
612        let back: Stamp = bytes.try_into().unwrap();
613        assert_eq!(stamp, back);
614    }
615
616    /// Test recover_signer using the Go interop test vector.
617    ///
618    /// This uses the same test data as stamper::tests::test_verify_go_created_stamp
619    /// to ensure the Stamp::recover_signer method works correctly.
620    #[test]
621    fn test_recover_signer() {
622        // Test vector from Go's TestGenerateInteropStamp
623        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        // Test recover_signer
635        let recovered = stamp.recover_signer(&chunk_address).unwrap();
636        assert_eq!(recovered, expected_owner);
637    }
638
639    /// Test verify method using the Go interop test vector.
640    #[test]
641    fn test_verify() {
642        // Test vector from Go's TestGenerateInteropStamp
643        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        // Verify with correct owner should succeed
656        assert!(stamp.verify(&chunk_address, expected_owner).is_ok());
657
658        // Verify with wrong owner should fail
659        let result = stamp.verify(&chunk_address, wrong_owner);
660        assert!(matches!(result, Err(StampError::OwnerMismatch { .. })));
661    }
662
663    /// Test recover_pubkey using the Go interop test vector.
664    #[test]
665    fn test_recover_pubkey() {
666        use alloy_signer::utils::public_key_to_address;
667
668        // Test vector from Go's TestGenerateInteropStamp
669        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        // Test recover_pubkey
681        let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
682
683        // Convert to address and verify
684        let recovered_addr = public_key_to_address(&pubkey);
685        assert_eq!(recovered_addr, expected_owner);
686    }
687
688    /// Test verify_with_pubkey using the Go interop test vector.
689    #[test]
690    fn test_verify_with_pubkey() {
691        // Test vector from Go's TestGenerateInteropStamp
692        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        // First recover the public key
703        let pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
704
705        // Now verify using the cached pubkey
706        let result = stamp.verify_with_pubkey(&chunk_address, &pubkey);
707        assert!(result.is_ok());
708    }
709
710    /// Test that verify_with_pubkey fails with wrong pubkey.
711    #[test]
712    fn test_verify_with_wrong_pubkey() {
713        use alloy_signer::SignerSync;
714        use alloy_signer_local::PrivateKeySigner;
715
716        // Create a stamp with one signer
717        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        // sign_message_sync returns alloy_primitives::Signature directly
727        let sig = signer.sign_message_sync(prehash.as_slice()).unwrap();
728        let stamp = Stamp::with_index(batch_id, index, timestamp, sig);
729
730        // Get the correct pubkey
731        let correct_pubkey = stamp.recover_pubkey(&chunk_address).unwrap();
732
733        // Create a different signer for wrong pubkey
734        let wrong_signer = PrivateKeySigner::random();
735        let wrong_pubkey = wrong_signer.credential().verifying_key();
736
737        // Verify with correct pubkey should succeed
738        assert!(
739            stamp
740                .verify_with_pubkey(&chunk_address, &correct_pubkey)
741                .is_ok()
742        );
743
744        // Verify with wrong pubkey should fail
745        assert!(
746            stamp
747                .verify_with_pubkey(&chunk_address, wrong_pubkey)
748                .is_err()
749        );
750    }
751}