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