Skip to main content

nectar_primitives/chunk/
single_owner.rs

1//! Single-owner chunk implementation
2//!
3//! This module provides the implementation of single-owner chunks,
4//! which are chunks that include an owner identifier and signature.
5
6use alloy_primitives::{Address, B256, FixedBytes, Keccak256, Signature, address, b256, hex};
7use alloy_signer::SignerSync;
8use alloy_signer_local::PrivateKeySigner;
9use bytes::{Bytes, BytesMut};
10use std::fmt;
11use std::marker::PhantomData;
12
13use crate::PrimitivesError;
14use crate::bmt::DEFAULT_BODY_SIZE;
15use crate::cache::OnceCache;
16use crate::chunk::error::{self, ChunkError};
17use crate::error::Result;
18
19use super::bmt_body::BmtBody;
20use super::content::ContentChunk;
21use super::traits::{BmtChunk, Chunk, ChunkAddress, ChunkHeader, ChunkMetadata};
22
23// Constants for field sizes
24const ID_SIZE: usize = std::mem::size_of::<B256>();
25const SIGNATURE_SIZE: usize = 65;
26const MIN_SOC_FIELDS_SIZE: usize = ID_SIZE + SIGNATURE_SIZE;
27
28/// The address of the owner of the SOC for dispersed replicas.
29const DISPERSED_REPLICA_OWNER: Address = address!("0xdc5b20847f43d67928f49cd4f85d696b5a7617b5");
30/// Generated from the private key `0x0100000000000000000000000000000000000000000000000000000000000000`.
31const DISPERSED_REPLICA_OWNER_PK: B256 =
32    b256!("0x0100000000000000000000000000000000000000000000000000000000000000");
33
34/// A single-owner chunk with configurable body size.
35///
36/// This type represents a chunk of data that belongs to a specific owner
37/// and includes a digital signature proving ownership.
38#[derive(Debug, Clone)]
39pub struct SingleOwnerChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
40    /// The header containing type ID, version, and metadata (ID and signature)
41    header: SingleOwnerChunkHeader,
42    /// The body of the chunk, containing the actual data
43    body: BmtBody<BODY_SIZE>,
44    /// Cache for the chunk's address
45    chunk_address_cache: OnceCache<ChunkAddress>,
46    /// Cache for the chunk's owner address (derived from signature)
47    owner_cache: OnceCache<Address>,
48}
49
50/// Metadata for a single-owner chunk
51#[derive(Debug, Clone)]
52pub struct SingleOwnerChunkMetadata {
53    /// Unique identifier for this chunk
54    id: B256,
55    /// Digital signature of the chunk's ID and body hash
56    signature: Signature,
57}
58
59impl SingleOwnerChunkMetadata {
60    /// Create a new metadata instance with the given ID and signature
61    pub const fn new(id: B256, signature: Signature) -> Self {
62        Self { id, signature }
63    }
64
65    /// Get the unique ID of this chunk
66    pub const fn id(&self) -> B256 {
67        self.id
68    }
69
70    /// Get the signature of this chunk
71    pub const fn signature(&self) -> &Signature {
72        &self.signature
73    }
74}
75
76impl ChunkMetadata for SingleOwnerChunkMetadata {
77    fn bytes(&self) -> Bytes {
78        let mut bytes = BytesMut::with_capacity(ID_SIZE + SIGNATURE_SIZE);
79        bytes.extend_from_slice(self.id.as_ref());
80        bytes.extend_from_slice(&self.signature.as_bytes());
81        bytes.freeze()
82    }
83}
84
85/// Header for a single-owner chunk
86#[derive(Debug, Clone)]
87pub struct SingleOwnerChunkHeader {
88    metadata: SingleOwnerChunkMetadata,
89}
90
91impl SingleOwnerChunkHeader {
92    /// Create a new header with the given metadata
93    pub const fn new(metadata: SingleOwnerChunkMetadata) -> Self {
94        Self { metadata }
95    }
96}
97
98impl ChunkHeader for SingleOwnerChunkHeader {
99    type Metadata = SingleOwnerChunkMetadata;
100
101    fn id(&self) -> u8 {
102        1
103    }
104
105    fn version(&self) -> u8 {
106        1
107    }
108
109    fn metadata(&self) -> &Self::Metadata {
110        &self.metadata
111    }
112
113    fn bytes(&self) -> Bytes {
114        self.metadata.bytes()
115    }
116}
117
118impl<const BODY_SIZE: usize> SingleOwnerChunk<BODY_SIZE> {
119    /// Create a new single-owner chunk with the given ID, data, and signer.
120    ///
121    /// This function automatically calculates the span based on the data length
122    /// and signs the chunk using the provided signer.
123    ///
124    /// # Arguments
125    ///
126    /// * `id` - The unique identifier for this chunk.
127    /// * `data` - The raw data content to encapsulate in the chunk.
128    /// * `signer` - The signer to use for signing the chunk.
129    ///
130    /// # Returns
131    ///
132    /// A Result containing the new SingleOwnerChunk, or an error if creation fails.
133    #[must_use = "this returns a new chunk without modifying the input"]
134    pub fn new(id: B256, data: impl Into<Bytes>, signer: &impl SignerSync) -> Result<Self> {
135        SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
136            .auto_from_data(data)?
137            .with_id(id)
138            .with_signer(signer)?
139            .build()
140    }
141
142    /// Create a new SingleOwnerChunk with a pre-signed signature.
143    ///
144    /// This function is useful when the signature is already known, for example
145    /// when retrieving a chunk from a database or when reconstructing after verification.
146    ///
147    /// # Arguments
148    ///
149    /// * `id` - The unique identifier for this chunk.
150    /// * `signature` - The pre-computed signature.
151    /// * `data` - The raw data content to encapsulate in the chunk.
152    ///
153    /// # Returns
154    ///
155    /// A Result containing the new SingleOwnerChunk, or an error if creation fails.
156    #[must_use = "this returns a new chunk without modifying the input"]
157    pub fn with_signature(id: B256, signature: Signature, data: impl Into<Bytes>) -> Result<Self> {
158        SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
159            .auto_from_data(data)?
160            .with_id(id)
161            .with_signature(signature)?
162            .build()
163    }
164
165    /// Create a new `SingleOwnerChunk` as a dispersed replica.
166    ///
167    /// # Arguments
168    /// * `mined_byte` - The first byte of the chunk's ID.
169    /// * `body` - The underlying BMT body containing the data and metadata.
170    #[must_use = "this returns a new chunk without modifying the input"]
171    pub fn new_dispersed_replica(mined_byte: u8, body: BmtBody<BODY_SIZE>) -> Result<Self> {
172        SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
173            .with_body(body)
174            .dispersed_replica(mined_byte)?
175            .build()
176    }
177
178    /// Create a SingleOwnerChunk from pre-computed parts.
179    ///
180    /// This is an advanced method for reconstructing chunks from storage
181    /// when you have all the individual components.
182    ///
183    /// # Arguments
184    ///
185    /// * `id` - The chunk's unique identifier.
186    /// * `signature` - The digital signature.
187    /// * `body` - The BMT body containing the data.
188    #[must_use]
189    pub const fn from_parts(id: B256, signature: Signature, body: BmtBody<BODY_SIZE>) -> Self {
190        let metadata = SingleOwnerChunkMetadata::new(id, signature);
191        let header = SingleOwnerChunkHeader::new(metadata);
192
193        Self {
194            header,
195            body,
196            chunk_address_cache: OnceCache::new(),
197            owner_cache: OnceCache::new(),
198        }
199    }
200
201    /// Create a SingleOwnerChunk from pre-computed parts with cached address and owner.
202    ///
203    /// This is an advanced method for reconstructing chunks when you also know
204    /// the chunk address and owner address.
205    #[must_use]
206    pub fn from_parts_with_caches(
207        id: B256,
208        signature: Signature,
209        body: BmtBody<BODY_SIZE>,
210        address: ChunkAddress,
211        owner: Address,
212    ) -> Self {
213        let metadata = SingleOwnerChunkMetadata::new(id, signature);
214        let header = SingleOwnerChunkHeader::new(metadata);
215
216        Self {
217            header,
218            body,
219            chunk_address_cache: OnceCache::with_value(address),
220            owner_cache: OnceCache::with_value(owner),
221        }
222    }
223
224    /// Get the owner's address, derived from the signature.
225    ///
226    /// This computes the owner's address by recovering it from the signature
227    /// and the signed data (the chunk's ID and body hash). The result is cached
228    /// on success for subsequent calls.
229    ///
230    /// # Returns
231    ///
232    /// The owner's address as a 20-byte fixed array, or an error if signature
233    /// recovery fails.
234    ///
235    /// # Errors
236    ///
237    /// Returns `ChunkError::Signature` if the signature recovery fails.
238    pub fn owner(&self) -> error::Result<Address> {
239        // Check if we have a cached value
240        if let Some(addr) = self.owner_cache.get() {
241            return Ok(*addr);
242        }
243
244        // Compute and cache on success (don't cache failures)
245        let addr = self.calculate_owner()?;
246        // Try to set the cache; ignore if another thread beat us
247        let _ = self.owner_cache.try_set(addr);
248        Ok(addr)
249    }
250
251    /// Calculate the owner's address from the signature.
252    fn calculate_owner(&self) -> error::Result<Address> {
253        // Generate the hash to verify
254        let hash = Self::to_sign(&self.header.metadata.id, &self.body);
255
256        // Recover the address from the signature
257        self.signature()
258            .recover_address_from_msg(hash)
259            .map_err(Into::into)
260    }
261
262    /// Compute the data to be signed for this chunk.
263    ///
264    /// This combines the chunk's ID and body hash to create the data
265    /// that is signed to prove ownership.
266    ///
267    /// # Arguments
268    ///
269    /// * `id` - The chunk's ID.
270    /// * `body` - The chunk's body.
271    ///
272    /// # Returns
273    ///
274    /// A 32-byte hash representing the data to sign.
275    fn to_sign(id: &B256, body: &BmtBody<BODY_SIZE>) -> B256 {
276        let mut hasher = Keccak256::new();
277        hasher.update(id);
278        hasher.update(body.hash());
279        hasher.finalize()
280    }
281
282    // Checks if the chunk is a valid dispersed replica
283    fn is_valid_replica(&self) -> bool {
284        self.id()[1..] == self.body.hash().as_slice()[1..]
285    }
286
287    /// Get the ID of this chunk.
288    pub const fn id(&self) -> B256 {
289        self.header.metadata.id
290    }
291
292    /// Get the signature of this chunk.
293    pub const fn signature(&self) -> &Signature {
294        &self.header.metadata.signature
295    }
296
297    /// Borrow the inner content body wrapped by this single-owner chunk.
298    ///
299    /// A single-owner chunk is `id || signature || span || payload`; the
300    /// `span || payload` tail is exactly the [`BmtBody`] of the content chunk it
301    /// wraps. This is the zero-copy accessor for that body, so callers reading
302    /// the wrapped span/payload never re-slice past the `id`/`signature`
303    /// header.
304    pub const fn inner_body(&self) -> &BmtBody<BODY_SIZE> {
305        &self.body
306    }
307
308    /// Extract the content-addressed chunk (CAC) wrapped inside this SOC.
309    ///
310    /// Mirrors bee's `soc.UnwrapCAC` (`pkg/soc/soc.go`): the SOC body *is* a
311    /// CAC body (`span || payload`), so this rebuilds the [`ContentChunk`] from
312    /// it without any manual `HashSize + SignatureSize` cursor arithmetic. The
313    /// returned chunk's address is the wrapped content address, not the SOC
314    /// address.
315    #[must_use]
316    pub fn unwrap_cac(&self) -> ContentChunk<BODY_SIZE> {
317        ContentChunk::from_body(self.body.clone())
318    }
319}
320
321impl<const BODY_SIZE: usize> Chunk for SingleOwnerChunk<BODY_SIZE> {
322    type Header = SingleOwnerChunkHeader;
323
324    fn address(&self) -> &ChunkAddress {
325        self.chunk_address_cache.get_or_compute(|| {
326            // Compute address from id and owner
327            // Note: If owner recovery fails, we use Address::ZERO which will cause
328            // address verification to fail, which is the correct behavior.
329            let owner = self.owner().unwrap_or(Address::ZERO);
330            let mut hasher = Keccak256::new();
331            hasher.update(self.id());
332            hasher.update(owner);
333
334            hasher.finalize().into()
335        })
336    }
337
338    fn data(&self) -> &Bytes {
339        self.body.data()
340    }
341
342    fn size(&self) -> usize {
343        self.header().bytes().len() + self.body.size()
344    }
345
346    fn header(&self) -> &Self::Header {
347        &self.header
348    }
349
350    fn verify(&self, expected: &ChunkAddress) -> Result<()> {
351        let actual = self.address();
352
353        // At this point, the owner has been recovered. Now check if the owner
354        // is the replica chunk owner, the ID must adhere to specific semantics.
355        let owner = self.owner()?;
356        if owner == DISPERSED_REPLICA_OWNER && !self.is_valid_replica() {
357            return Err(error::ChunkError::invalid_format("invalid dispersed replica").into());
358        }
359
360        if actual != expected {
361            return Err(error::ChunkError::verification_failed(*expected, *actual).into());
362        }
363        Ok(())
364    }
365}
366
367impl<const BODY_SIZE: usize> BmtChunk for SingleOwnerChunk<BODY_SIZE> {
368    fn span(&self) -> u64 {
369        self.body.span()
370    }
371}
372
373impl<const BODY_SIZE: usize> From<SingleOwnerChunk<BODY_SIZE>> for Bytes {
374    fn from(chunk: SingleOwnerChunk<BODY_SIZE>) -> Self {
375        let mut bytes = BytesMut::with_capacity(chunk.size());
376        bytes.extend_from_slice(chunk.header().bytes().as_ref());
377        bytes.extend_from_slice(&Self::from(chunk.body));
378        bytes.freeze()
379    }
380}
381
382impl<const BODY_SIZE: usize> TryFrom<Bytes> for SingleOwnerChunk<BODY_SIZE> {
383    type Error = PrimitivesError;
384
385    fn try_from(bytes: Bytes) -> Result<Self> {
386        if bytes.len() < MIN_SOC_FIELDS_SIZE {
387            return Err(ChunkError::invalid_size(
388                "insufficient data for single-owner chunk",
389                MIN_SOC_FIELDS_SIZE,
390                bytes.len(),
391            )
392            .into());
393        }
394
395        // Extract ID
396        let id_slice = &bytes.slice(0..ID_SIZE);
397        let mut id = FixedBytes::<32>::default();
398        id.copy_from_slice(id_slice);
399
400        // Extract signature
401        let sig_slice = &bytes.slice(ID_SIZE..ID_SIZE + SIGNATURE_SIZE);
402        let signature = Signature::from_raw(sig_slice).map_err(ChunkError::from)?;
403
404        // Extract body
405        let body_bytes = bytes.slice(ID_SIZE + SIGNATURE_SIZE..);
406        let body = BmtBody::try_from(body_bytes)?;
407
408        // Create metadata and header
409        let metadata = SingleOwnerChunkMetadata::new(id, signature);
410        let header = SingleOwnerChunkHeader::new(metadata);
411
412        Ok(Self {
413            header,
414            body,
415            chunk_address_cache: OnceCache::new(),
416            owner_cache: OnceCache::new(),
417        })
418    }
419}
420
421impl<const BODY_SIZE: usize> TryFrom<&[u8]> for SingleOwnerChunk<BODY_SIZE> {
422    type Error = PrimitivesError;
423
424    fn try_from(bytes: &[u8]) -> Result<Self> {
425        Self::try_from(Bytes::copy_from_slice(bytes))
426    }
427}
428
429impl<const BODY_SIZE: usize> fmt::Display for SingleOwnerChunk<BODY_SIZE> {
430    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431        let owner_str = self.owner().map_or_else(
432            |_| "invalid".to_string(),
433            |addr| hex::encode(addr.as_slice()),
434        );
435        write!(
436            f,
437            "SingleOwnerChunk[id={}, owner={}]",
438            hex::encode(&self.id()[..8]),
439            owner_str
440        )
441    }
442}
443
444impl<const BODY_SIZE: usize> PartialEq for SingleOwnerChunk<BODY_SIZE> {
445    fn eq(&self, other: &Self) -> bool {
446        // If either owner computation fails, chunks are not equal
447        match (self.owner(), other.owner()) {
448            (Ok(a), Ok(b)) => self.id() == other.id() && a == b,
449            _ => false,
450        }
451    }
452}
453
454impl<const BODY_SIZE: usize> Eq for SingleOwnerChunk<BODY_SIZE> {}
455
456impl<const BODY_SIZE: usize> super::chunk_type::ChunkType for SingleOwnerChunk<BODY_SIZE> {
457    const TYPE_ID: super::type_id::ChunkTypeId = super::type_id::ChunkTypeId::SINGLE_OWNER;
458    const TYPE_NAME: &'static str = "single_owner";
459}
460
461// Internal builder state marker traits
462trait BuilderState {}
463
464#[derive(Debug, Default)]
465struct Initial;
466impl BuilderState for Initial {}
467
468#[derive(Debug)]
469struct WithData;
470impl BuilderState for WithData {}
471
472#[derive(Debug)]
473struct WithId;
474impl BuilderState for WithId {}
475
476#[derive(Debug)]
477struct ReadyToBuild;
478impl BuilderState for ReadyToBuild {}
479
480/// Builder for SingleOwnerChunk with type state pattern
481#[derive(Debug)]
482struct SingleOwnerChunkBuilderImpl<const BODY_SIZE: usize, S: BuilderState = Initial> {
483    /// The body to use for the chunk
484    body: Option<BmtBody<BODY_SIZE>>,
485    /// The ID to use for the chunk
486    id: Option<B256>,
487    /// The signature to use for the chunk
488    signature: Option<Signature>,
489    /// Marker for the builder state
490    _state: PhantomData<S>,
491}
492
493impl<const BODY_SIZE: usize> Default for SingleOwnerChunkBuilderImpl<BODY_SIZE, Initial> {
494    fn default() -> Self {
495        Self {
496            body: None,
497            id: None,
498            signature: None,
499            _state: PhantomData,
500        }
501    }
502}
503
504impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, Initial> {
505    /// Initialize from data with automatically calculated span
506    fn auto_from_data(
507        mut self,
508        data: impl Into<Bytes>,
509    ) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData>> {
510        let body = BmtBody::<BODY_SIZE>::builder()
511            .auto_from_data(data)?
512            .build()?;
513        self.body = Some(body);
514
515        Ok(SingleOwnerChunkBuilderImpl {
516            body: self.body,
517            id: self.id,
518            signature: self.signature,
519            _state: PhantomData,
520        })
521    }
522
523    /// Initialize with a specific body
524    fn with_body(
525        mut self,
526        body: BmtBody<BODY_SIZE>,
527    ) -> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData> {
528        self.body = Some(body);
529
530        SingleOwnerChunkBuilderImpl {
531            body: self.body,
532            id: self.id,
533            signature: self.signature,
534            _state: PhantomData,
535        }
536    }
537}
538
539impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithData> {
540    /// Set the ID for this chunk
541    fn with_id(mut self, id: B256) -> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithId> {
542        self.id = Some(id);
543
544        SingleOwnerChunkBuilderImpl {
545            body: self.body,
546            id: self.id,
547            signature: self.signature,
548            _state: PhantomData,
549        }
550    }
551
552    /// Creates a new dispersed replica chunk with the given first byte and transitions to ReadyToBuild
553    fn dispersed_replica(
554        self,
555        first_byte: u8,
556    ) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
557        let body_hash = self.body.as_ref().unwrap().hash();
558        let mut id = B256::default();
559        id[0] = first_byte;
560        id[1..].copy_from_slice(&body_hash.as_slice()[1..]);
561
562        let signer = PrivateKeySigner::from_slice(DISPERSED_REPLICA_OWNER_PK.as_slice()).unwrap();
563
564        self.with_id(id).with_signer(&signer)
565    }
566}
567
568impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, WithId> {
569    /// Sign the chunk with the given signer
570    fn with_signer(
571        self,
572        signer: &impl SignerSync,
573    ) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
574        // Get body and ID - these are guaranteed to be Some by the state
575        let body = self.body.as_ref().unwrap();
576        let id = self.id.as_ref().unwrap();
577
578        // Compute hash to sign
579        let hash = SingleOwnerChunk::<BODY_SIZE>::to_sign(id, body);
580
581        // Sign the hash
582        let signature = signer
583            .sign_message_sync(hash.as_ref())
584            .map_err(ChunkError::from)?;
585
586        self.with_signature(signature)
587    }
588
589    /// Set a pre-computed signature
590    fn with_signature(
591        mut self,
592        signature: Signature,
593    ) -> Result<SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild>> {
594        self.signature = Some(signature);
595
596        Ok(SingleOwnerChunkBuilderImpl {
597            body: self.body,
598            id: self.id,
599            signature: self.signature,
600            _state: PhantomData,
601        })
602    }
603}
604
605impl<const BODY_SIZE: usize> SingleOwnerChunkBuilderImpl<BODY_SIZE, ReadyToBuild> {
606    /// Build the final SingleOwnerChunk
607    fn build(self) -> Result<SingleOwnerChunk<BODY_SIZE>> {
608        let body = self.body.unwrap();
609        let id = self.id.unwrap();
610        let signature = self.signature.unwrap();
611
612        Ok(SingleOwnerChunk::from_parts(id, signature, body))
613    }
614}
615
616#[cfg(any(test, feature = "arbitrary"))]
617impl<'a, const BODY_SIZE: usize> arbitrary::Arbitrary<'a> for SingleOwnerChunk<BODY_SIZE> {
618    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
619        let id = B256::arbitrary(u)?;
620        let body = BmtBody::<BODY_SIZE>::arbitrary(u)?;
621        let signer = alloy_signer_local::PrivateKeySigner::random();
622
623        Ok(SingleOwnerChunkBuilderImpl::<BODY_SIZE, Initial>::default()
624            .with_body(body)
625            .with_id(id)
626            .with_signer(&signer)
627            .unwrap()
628            .build()
629            .unwrap())
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use crate::DEFAULT_BODY_SIZE;
636
637    use super::*;
638    use alloy_primitives::hex;
639    use proptest::prelude::*;
640    use proptest_arbitrary_interop::arb;
641
642    type DefaultSingleOwnerChunk = SingleOwnerChunk<DEFAULT_BODY_SIZE>;
643
644    fn get_test_wallet() -> PrivateKeySigner {
645        // Test private key corresponding to address 0x8d3766440f0d7b949a5e32995d09619a7f86e632
646        let pk = hex!("2c7536e3605d9c16a7a3d7b1898e529396a65c23a3bcbd4012a11cf2731b0fbc");
647        PrivateKeySigner::from_slice(&pk).unwrap()
648    }
649
650    // Strategy for generating SingleOwnerChunk using the Arbitrary implementation
651    fn chunk_strategy() -> impl Strategy<Value = DefaultSingleOwnerChunk> {
652        arb::<DefaultSingleOwnerChunk>()
653    }
654
655    proptest! {
656        #[test]
657        fn test_chunk_properties(chunk in chunk_strategy()) {
658            prop_assert!(chunk.size() >= MIN_SOC_FIELDS_SIZE);
659
660            // Test round-trip conversion
661            let bytes: Bytes = chunk.clone().into();
662            let decoded = DefaultSingleOwnerChunk::try_from(bytes.as_ref()).unwrap();
663            prop_assert_eq!(chunk.id(), decoded.id());
664            prop_assert_eq!(chunk.signature(), decoded.signature());
665            prop_assert_eq!(chunk.data(), decoded.data());
666            prop_assert_eq!(chunk.owner().unwrap(), decoded.owner().unwrap());
667
668            // Test address verification
669            let address = chunk.address();
670            prop_assert!(chunk.verify(address).is_ok());
671        }
672
673        #[test]
674        fn test_dispersed_replica_properties(first_byte in any::<u8>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
675            let chunk = DefaultSingleOwnerChunk::new_dispersed_replica(first_byte, BmtBody::<DEFAULT_BODY_SIZE>::builder().auto_from_data(data).unwrap().build().unwrap()).unwrap();
676
677            // Verify it's recognised as a dispersed replica
678            prop_assert!(chunk.is_valid_replica());
679            prop_assert_eq!(chunk.id()[0], first_byte);
680            prop_assert_eq!(chunk.owner().unwrap(), DISPERSED_REPLICA_OWNER);
681
682            // Verify chunk address
683            prop_assert!(chunk.verify(chunk.address()).is_ok());
684        }
685
686        #[test]
687        fn test_chunk_creation(id in arb::<B256>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
688            let wallet = get_test_wallet();
689
690            // Test creation through builder
691            let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
692                .with_body(
693                    BmtBody::<DEFAULT_BODY_SIZE>::builder()
694                        .auto_from_data(data.clone())
695                        .unwrap()
696                        .build()
697                        .unwrap(),
698                )
699                .with_id(id)
700                .with_signer(&wallet)
701                .unwrap()
702                .build()
703                .unwrap();
704
705            prop_assert_eq!(chunk.id(), id);
706            prop_assert_eq!(chunk.data(), &data);
707            prop_assert!(!chunk.owner().unwrap().is_zero());
708        }
709
710        #[test]
711        fn test_dispersed_replica_mismatched_address(first_byte in any::<u8>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
712            let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default().with_body(
713                BmtBody::<DEFAULT_BODY_SIZE>::builder()
714                    .auto_from_data(data)
715                    .unwrap()
716                    .build()
717                    .unwrap(),
718            ).dispersed_replica(first_byte).unwrap().build().unwrap();
719            let replica_address = *chunk.address();
720            // Serialise the chunk
721            let bytes: Bytes = chunk.into();
722
723            // Modify the ID (31 bytes), except the first byte to be random.
724            // This should make the chunk not recognised as a dispersed replica
725            let mut modified_bytes = bytes.to_vec();
726            modified_bytes[1..ID_SIZE].copy_from_slice(&[0x01; 31]);
727
728            let modified_chunk = DefaultSingleOwnerChunk::try_from(modified_bytes.as_slice()).unwrap();
729            prop_assert!(!modified_chunk.is_valid_replica());
730            prop_assert!(modified_chunk.verify(&replica_address).is_err());
731        }
732
733        #[test]
734        fn test_chunk_invalid_signature(id in arb::<B256>(), data in proptest::collection::vec(any::<u8>(), 1..DEFAULT_BODY_SIZE)) {
735            let wallet = get_test_wallet();
736
737            // Test creation through builder
738            let chunk = DefaultSingleOwnerChunk::new(id, data, &wallet).unwrap();
739            let original_address = *chunk.address();
740
741            // Serialise the chunk
742            let bytes: Bytes = chunk.into();
743
744            // Modify the signature (65 bytes), except the first byte to be random.
745            // This should make the chunk not recognised as a dispersed replica
746            let mut modified_bytes = bytes.to_vec();
747            modified_bytes[ID_SIZE..ID_SIZE + 65].copy_from_slice(&[0xff; 65]);
748
749            let modified_chunk = DefaultSingleOwnerChunk::try_from(modified_bytes.as_slice()).unwrap();
750            prop_assert!(modified_chunk.verify(&original_address).is_err());
751            // Owner recovery should fail for invalid signature
752            prop_assert!(modified_chunk.owner().is_err());
753        }
754
755        #[test]
756        fn test_chunk_too_small(data in proptest::collection::vec(any::<u8>(), 1..MIN_SOC_FIELDS_SIZE)) {
757            // Test insufficient data size
758            let chunk = DefaultSingleOwnerChunk::try_from(data.as_slice());
759            prop_assert!(chunk.is_err());
760        }
761    }
762
763    #[test]
764    fn test_new() {
765        let id = B256::ZERO;
766        let data = b"foo".to_vec();
767        let wallet = get_test_wallet();
768
769        let chunk = DefaultSingleOwnerChunk::new(id, data.clone(), &wallet).unwrap();
770
771        assert_eq!(chunk.id(), id);
772        assert_eq!(chunk.data(), &data);
773    }
774
775    #[test]
776    fn test_new_signed() {
777        let id = B256::ZERO;
778        let data = b"foo".to_vec();
779
780        // Known good signature from Go tests
781        let sig = hex!(
782            "5acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a053deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c1348311b"
783        );
784        let signature = Signature::try_from(sig.as_slice()).unwrap();
785
786        let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
787            .auto_from_data(data.clone())
788            .unwrap()
789            .with_id(id)
790            .with_signature(signature)
791            .unwrap()
792            .build()
793            .unwrap();
794
795        assert_eq!(chunk.id(), id);
796        assert_eq!(chunk.data(), &data);
797        assert_eq!(chunk.signature().as_bytes(), sig);
798
799        // Verify owner address matches expected
800        let expected_owner = address!("8d3766440f0d7b949a5e32995d09619a7f86e632");
801        assert_eq!(chunk.owner().unwrap(), expected_owner);
802    }
803
804    fn get_test_chunk_data() -> Vec<u8> {
805        hex!(
806            "000000000000000000000000000000000000000000000000000000000000000\
807            05acd384febc133b7b245e5ddc62d82d2cded9182d2716126cd8844509af65a05\
808            3deb418208027f548e3e88343af6f84a8772fb3cebc0a1833a0ea7ec0c134831\
809            1b0300000000000000666f6f"
810        )
811        .to_vec()
812    }
813
814    #[test]
815    fn test_chunk_address() {
816        // Should parse successfully
817        let chunk = DefaultSingleOwnerChunk::try_from(get_test_chunk_data().as_slice()).unwrap();
818
819        // Verify expected owner
820        let expected_owner = address!("8d3766440f0d7b949a5e32995d09619a7f86e632");
821        assert_eq!(chunk.owner().unwrap(), expected_owner);
822
823        // Verify expected address
824        let expected_address =
825            b256!("9d453ebb73b2fedaaf44ceddcf7a0aa37f3e3d6453fea5841c31f0ea6d61dc85");
826        assert_eq!(chunk.address().as_ref(), expected_address);
827    }
828
829    #[test]
830    fn test_invalid_dispersed_replica() -> Result<()> {
831        let test_data = b"test data".to_vec();
832        let dispersed_replica_wallet =
833            PrivateKeySigner::from_slice(DISPERSED_REPLICA_OWNER_PK.as_slice()).unwrap();
834
835        let chunk = SingleOwnerChunkBuilderImpl::<DEFAULT_BODY_SIZE, Initial>::default()
836            .with_body(
837                BmtBody::<DEFAULT_BODY_SIZE>::builder()
838                    .auto_from_data(test_data)?
839                    .build()?,
840            )
841            .with_id(B256::ZERO)
842            .with_signer(&dispersed_replica_wallet)?
843            .build()?;
844        let replica_address = chunk.address();
845
846        assert!(!chunk.is_valid_replica());
847        assert!(matches!(
848            chunk.verify(replica_address),
849            Err(PrimitivesError::Chunk(ChunkError::InvalidFormat { .. }))
850        ));
851
852        Ok(())
853    }
854}