Skip to main content

nectar_primitives/chunk/
any_chunk.rs

1//! Type-erased chunk type
2//!
3//! This module provides [`AnyChunk`], an enum that can hold any chunk type
4//! for runtime polymorphism without requiring trait objects.
5
6use alloy_primitives::Keccak256;
7use bytes::Bytes;
8
9use crate::bmt::DEFAULT_BODY_SIZE;
10use crate::error::Result;
11
12use super::chunk_type::ChunkType;
13use super::content::ContentChunk;
14use super::single_owner::SingleOwnerChunk;
15use super::traits::{Chunk, ChunkAddress};
16use super::type_id::ChunkTypeId;
17
18/// Type-erased chunk for runtime polymorphism with configurable body size.
19///
20/// This enum provides dynamic dispatch for chunks without requiring object-safe traits.
21/// Use this when you need to store heterogeneous chunk types in collections or pass
22/// chunks through interfaces that can't be generic.
23///
24/// # Why an enum instead of `Box<dyn Chunk>`?
25///
26/// The [`Chunk`] trait has an associated type (`type Header`) which makes it not
27/// object-safe. This enum provides the same functionality while maintaining type safety.
28///
29/// # Examples
30///
31/// ```
32/// use nectar_primitives::{AnyChunk, Chunk, ContentChunk, ChunkTypeId};
33///
34/// // Create a content chunk
35/// let content = ContentChunk::new(&b"hello world"[..]).unwrap();
36/// let any: AnyChunk = content.clone().into();
37///
38/// // Access common properties
39/// assert_eq!(any.type_id(), ChunkTypeId::CONTENT);
40///
41/// // Get the concrete type back
42/// if let Some(recovered) = any.as_content() {
43///     assert_eq!(recovered.address(), content.address());
44/// }
45/// ```
46#[derive(Debug, Clone)]
47pub enum AnyChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
48    /// A content-addressed chunk (CAC).
49    Content(ContentChunk<BODY_SIZE>),
50    /// A single-owner chunk (SOC).
51    SingleOwner(SingleOwnerChunk<BODY_SIZE>),
52}
53
54impl<const BODY_SIZE: usize> AnyChunk<BODY_SIZE> {
55    /// Get the address of this chunk.
56    pub fn address(&self) -> &ChunkAddress {
57        match self {
58            Self::Content(c) => c.address(),
59            Self::SingleOwner(c) => c.address(),
60        }
61    }
62
63    /// Get the raw data contained in this chunk.
64    pub fn data(&self) -> &Bytes {
65        match self {
66            Self::Content(c) => c.data(),
67            Self::SingleOwner(c) => c.data(),
68        }
69    }
70
71    /// Get the type ID of this chunk.
72    pub const fn type_id(&self) -> ChunkTypeId {
73        match self {
74            Self::Content(_) => ChunkTypeId::CONTENT,
75            Self::SingleOwner(_) => ChunkTypeId::SINGLE_OWNER,
76        }
77    }
78
79    /// Get the total size of this chunk in bytes.
80    pub fn size(&self) -> usize {
81        match self {
82            Self::Content(c) => c.size(),
83            Self::SingleOwner(c) => c.size(),
84        }
85    }
86
87    /// Get the span (logical data length) of this chunk: the BMT span of its
88    /// underlying body.
89    pub fn span(&self) -> u64 {
90        match self {
91            Self::Content(c) => super::traits::BmtChunk::span(c),
92            Self::SingleOwner(c) => super::traits::BmtChunk::span(c),
93        }
94    }
95
96    /// Compute the anchor-keyed *transformed address* of this chunk.
97    ///
98    /// The transformed address is the redistribution sampler's per-round,
99    /// per-node re-hash of a chunk. It is a prefixed BMT root keyed by the
100    /// node's `anchor`, used to order reserve chunks deterministically while
101    /// binding the ordering to the proving node. This reproduces bee's
102    /// `storer.transformedAddress` (`pkg/storer/sample.go`) byte-for-byte.
103    ///
104    /// # Derivation
105    ///
106    /// - For a content-addressed chunk (CAC) the transformed address is the
107    ///   prefixed BMT of the chunk body, i.e. `BMT(prefix = anchor, span,
108    ///   payload)`. This is computed by
109    ///   [`BmtBody::transformed_root`](super::bmt_body::BmtBody::transformed_root) on the
110    ///   chunk's borrowed body, which mixes the anchor into *every* node hash,
111    ///   matching bee's prefix hasher.
112    /// - For a single-owner chunk (SOC) the wrapped content body is re-hashed
113    ///   the same way to obtain `inner`, then the SOC's transformed address is
114    ///   a **plain, unprefixed** `keccak256(soc_address || inner)`. The outer
115    ///   wrap is a flat Keccak256, *not* a prefixed BMT, mirroring bee's
116    ///   `transformedAddressSOC` which uses `swarm.NewHasher()` (no anchor) for
117    ///   the final combine.
118    ///
119    /// # Endianness
120    ///
121    /// The span is serialised little-endian inside the BMT. Do not confuse this
122    /// with the big-endian encodings used elsewhere on the redistribution wire
123    /// (e.g. proof witness indices); the BMT span is always LE.
124    ///
125    /// # Borrowing
126    ///
127    /// The CAC and SOC paths dispatch through borrowed body accessors
128    /// ([`ContentChunk::body`] and [`SingleOwnerChunk::inner_body`]), so no
129    /// chunk or body is cloned. For a SOC the wrapped body already *is* the
130    /// content chunk's `span || payload`, so the inner root needs no
131    /// `32 + 65` (id + signature) header slicing.
132    pub fn transformed_address(&self, anchor: &[u8]) -> ChunkAddress {
133        match self {
134            // CAC: the prefixed BMT root of the (borrowed) body is the address.
135            Self::Content(c) => ChunkAddress::from(c.body().transformed_root(anchor)),
136            // SOC: re-hash the (borrowed) wrapped body, then take the plain
137            // (unprefixed) keccak256(soc_address || inner).
138            Self::SingleOwner(c) => {
139                let inner = c.inner_body().transformed_root(anchor);
140                let mut hasher = Keccak256::new();
141                hasher.update(c.address().as_slice());
142                hasher.update(inner.as_slice());
143                ChunkAddress::from(hasher.finalize())
144            }
145        }
146    }
147
148    /// Verify that this chunk's address matches an expected address.
149    pub fn verify(&self, expected: &ChunkAddress) -> Result<()> {
150        match self {
151            Self::Content(c) => c.verify(expected),
152            Self::SingleOwner(c) => c.verify(expected),
153        }
154    }
155
156    /// Convert this chunk into its serialized bytes representation.
157    pub fn into_bytes(self) -> Bytes {
158        match self {
159            Self::Content(c) => c.into(),
160            Self::SingleOwner(c) => c.into(),
161        }
162    }
163
164    /// Check if this chunk is of a specific type.
165    pub fn is<T: ChunkType>(&self) -> bool {
166        self.type_id() == T::TYPE_ID
167    }
168
169    /// Check if this is a content chunk.
170    pub const fn is_content(&self) -> bool {
171        matches!(self, Self::Content(_))
172    }
173
174    /// Check if this is a single-owner chunk.
175    pub const fn is_single_owner(&self) -> bool {
176        matches!(self, Self::SingleOwner(_))
177    }
178
179    /// Get a reference to the contained ContentChunk, if this is one.
180    pub const fn as_content(&self) -> Option<&ContentChunk<BODY_SIZE>> {
181        match self {
182            Self::Content(c) => Some(c),
183            _ => None,
184        }
185    }
186
187    /// Get a reference to the contained SingleOwnerChunk, if this is one.
188    pub const fn as_single_owner(&self) -> Option<&SingleOwnerChunk<BODY_SIZE>> {
189        match self {
190            Self::SingleOwner(c) => Some(c),
191            _ => None,
192        }
193    }
194
195    /// Convert into the contained ContentChunk, if this is one.
196    pub fn into_content(self) -> Option<ContentChunk<BODY_SIZE>> {
197        match self {
198            Self::Content(c) => Some(c),
199            _ => None,
200        }
201    }
202
203    /// Convert into the contained SingleOwnerChunk, if this is one.
204    pub fn into_single_owner(self) -> Option<SingleOwnerChunk<BODY_SIZE>> {
205        match self {
206            Self::SingleOwner(c) => Some(c),
207            _ => None,
208        }
209    }
210
211    /// Encode this chunk as a type-tagged, self-describing byte string.
212    ///
213    /// The layout is `[type_id: 1 byte][chunk wire bytes]`, where the chunk
214    /// wire bytes are the same form produced by [`AnyChunk::into_bytes`] (the
215    /// inverse of [`ContentChunk::try_from`]/[`SingleOwnerChunk::try_from`]).
216    ///
217    /// The chunk address is deliberately *not* embedded. It is supplied from
218    /// context on decode (for example the redb key when reading from storage,
219    /// or the address field of a wire message). This mirrors the existing
220    /// reconstruction paths that take an expected address.
221    ///
222    /// Unlike the address-disambiguating reconstruction path, decoding the
223    /// result of this method dispatches purely by `type_id`, so it never has
224    /// to trial-parse both the content and single-owner shapes.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use nectar_primitives::{AnyChunk, Chunk, ContentChunk};
230    ///
231    /// let content = ContentChunk::new(&b"hello world"[..]).unwrap();
232    /// let address = *content.address();
233    /// let any: AnyChunk = content.into();
234    ///
235    /// let encoded = any.to_typed_bytes();
236    /// assert_eq!(encoded[0], 0); // CONTENT type id
237    ///
238    /// let decoded: AnyChunk = AnyChunk::from_typed_bytes(&address, &encoded).unwrap();
239    /// assert_eq!(decoded.address(), any.address());
240    /// ```
241    pub fn to_typed_bytes(&self) -> Vec<u8> {
242        let tag = self.type_id().as_u8();
243        // Clone is required because `into_bytes` consumes the chunk; chunk
244        // payloads are reference-counted `Bytes`, so this is cheap.
245        let wire = self.clone().into_bytes();
246        let mut out = Vec::with_capacity(1 + wire.len());
247        out.push(tag);
248        out.extend_from_slice(&wire);
249        out
250    }
251
252    /// Decode a type-tagged chunk produced by [`AnyChunk::to_typed_bytes`].
253    ///
254    /// The leading byte selects the chunk type and the remainder is the chunk
255    /// wire payload. `address` is the expected chunk address (for example the
256    /// redb storage key or the wire message address field); the decoded chunk
257    /// is verified against it.
258    ///
259    /// Decoding dispatches by the type tag and therefore never trial-parses the
260    /// other chunk shape. Only the standard content and single-owner types are
261    /// recognised; any other tag is an error (custom chunk types are tracked
262    /// separately as a future registration mechanism).
263    ///
264    /// # Errors
265    ///
266    /// Returns an error (and never panics) when:
267    /// - the input is empty (no type tag),
268    /// - the type tag is not a recognised standard chunk type,
269    /// - the payload cannot be decoded as the tagged chunk type, or
270    /// - a standard chunk's computed address does not match `address`.
271    pub fn from_typed_bytes(address: &ChunkAddress, bytes: &[u8]) -> crate::error::Result<Self> {
272        let (&tag, payload) = bytes.split_first().ok_or_else(|| {
273            super::error::ChunkError::invalid_format("empty typed-chunk encoding: missing type tag")
274        })?;
275
276        let type_id = ChunkTypeId::from(tag);
277        match type_id {
278            ChunkTypeId::CONTENT => {
279                let chunk = ContentChunk::try_from(Bytes::copy_from_slice(payload))?;
280                chunk.verify(address)?;
281                Ok(Self::Content(chunk))
282            }
283            ChunkTypeId::SINGLE_OWNER => {
284                let chunk = SingleOwnerChunk::try_from(Bytes::copy_from_slice(payload))?;
285                chunk.verify(address)?;
286                Ok(Self::SingleOwner(chunk))
287            }
288            other => Err(super::error::ChunkError::unsupported_type(other).into()),
289        }
290    }
291
292    /// Decode a chunk from its bare wire bytes (NO type tag) given the expected
293    /// address, disambiguating content vs single-owner by which one hashes to
294    /// `address`. This is the type-less wire form (e.g. a Delivery `data`
295    /// field); prefer [`from_typed_bytes`](Self::from_typed_bytes) for
296    /// self-describing storage. Only content and single-owner shapes are
297    /// recognised; bytes that parse as neither (for the given address) error.
298    ///
299    /// Reconstructing an [`AnyChunk`] from raw bytes is ambiguous without the
300    /// address: a [`ContentChunk`] parse almost always succeeds structurally (a
301    /// span plus an arbitrary payload), so the expected address is the
302    /// disambiguator. The chunk is whichever variant parses *and* hashes to
303    /// `address`. Content is tried first, then single-owner; a lying address
304    /// makes both attempts fail, so the address is self-validating against the
305    /// bytes.
306    ///
307    /// # Errors
308    ///
309    /// Returns a verification error (and never panics) when neither the content
310    /// nor the single-owner interpretation of `data` hashes to `address`.
311    pub fn from_wire_bytes(address: &ChunkAddress, data: Bytes) -> crate::error::Result<Self> {
312        if let Ok(content) = ContentChunk::try_from(data.clone())
313            && content.address() == address
314        {
315            return Ok(Self::Content(content));
316        }
317        if let Ok(soc) = SingleOwnerChunk::try_from(data)
318            && soc.address() == address
319        {
320            return Ok(Self::SingleOwner(soc));
321        }
322        Err(super::error::ChunkError::verification_failed(*address, *address).into())
323    }
324}
325
326impl<const BODY_SIZE: usize> From<ContentChunk<BODY_SIZE>> for AnyChunk<BODY_SIZE> {
327    fn from(chunk: ContentChunk<BODY_SIZE>) -> Self {
328        Self::Content(chunk)
329    }
330}
331
332impl<const BODY_SIZE: usize> From<SingleOwnerChunk<BODY_SIZE>> for AnyChunk<BODY_SIZE> {
333    fn from(chunk: SingleOwnerChunk<BODY_SIZE>) -> Self {
334        Self::SingleOwner(chunk)
335    }
336}
337
338impl<const BODY_SIZE: usize> PartialEq for AnyChunk<BODY_SIZE> {
339    fn eq(&self, other: &Self) -> bool {
340        self.address() == other.address()
341    }
342}
343
344impl<const BODY_SIZE: usize> Eq for AnyChunk<BODY_SIZE> {}
345
346#[cfg(test)]
347mod tests {
348    use super::super::traits::{BmtChunk, Chunk};
349    use super::*;
350
351    type DefaultContentChunk = ContentChunk<DEFAULT_BODY_SIZE>;
352    type DefaultSingleOwnerChunk = SingleOwnerChunk<DEFAULT_BODY_SIZE>;
353    type DefaultAnyChunk = AnyChunk<DEFAULT_BODY_SIZE>;
354
355    #[test]
356    fn test_content_chunk_conversion() {
357        let content = DefaultContentChunk::new(&b"hello world"[..]).unwrap();
358        let address = *content.address();
359
360        let any: DefaultAnyChunk = content.into();
361
362        assert!(any.is_content());
363        assert!(!any.is_single_owner());
364        assert_eq!(any.type_id(), ChunkTypeId::CONTENT);
365        assert_eq!(*any.address(), address);
366    }
367
368    #[test]
369    fn test_as_content() {
370        let content = DefaultContentChunk::new(&b"test data"[..]).unwrap();
371        let expected_addr = *content.address();
372
373        let any: DefaultAnyChunk = content.into();
374        let recovered = any.as_content().unwrap();
375
376        assert_eq!(*recovered.address(), expected_addr);
377    }
378
379    #[test]
380    fn test_into_content() {
381        let content = DefaultContentChunk::new(&b"test data"[..]).unwrap();
382        let expected_addr = *content.address();
383
384        let any: DefaultAnyChunk = content.into();
385        let recovered = any.into_content().unwrap();
386
387        assert_eq!(*recovered.address(), expected_addr);
388    }
389
390    #[test]
391    fn test_is_methods() {
392        let content: DefaultAnyChunk = DefaultContentChunk::new(&b"test"[..]).unwrap().into();
393
394        assert!(content.is::<DefaultContentChunk>());
395        assert!(!content.is::<DefaultSingleOwnerChunk>());
396    }
397
398    #[test]
399    fn test_clone() {
400        let content = DefaultContentChunk::new(&b"test"[..]).unwrap();
401        let any: DefaultAnyChunk = content.into();
402        let cloned = any.clone();
403
404        assert_eq!(any.address(), cloned.address());
405        assert_eq!(any.type_id(), cloned.type_id());
406    }
407
408    fn test_signer() -> alloy_signer_local::PrivateKeySigner {
409        // Fixed key so addresses are deterministic across runs.
410        let pk = [0x42u8; 32];
411        alloy_signer_local::PrivateKeySigner::from_slice(&pk).unwrap()
412    }
413
414    fn sample_single_owner() -> DefaultSingleOwnerChunk {
415        let id = alloy_primitives::B256::ZERO;
416        DefaultSingleOwnerChunk::new(id, b"single owner payload".to_vec(), &test_signer()).unwrap()
417    }
418
419    #[test]
420    fn test_typed_content_round_trip() {
421        let content = DefaultContentChunk::new(&b"hello typed world"[..]).unwrap();
422        let address = *content.address();
423        let any: DefaultAnyChunk = content.into();
424
425        let encoded = any.to_typed_bytes();
426        assert_eq!(encoded[0], 0, "CONTENT tag must be 0");
427
428        let decoded = DefaultAnyChunk::from_typed_bytes(&address, &encoded).unwrap();
429        assert!(decoded.is_content());
430        assert_eq!(decoded.type_id(), ChunkTypeId::CONTENT);
431        assert_eq!(decoded.address(), any.address());
432        assert_eq!(decoded.data(), any.data());
433    }
434
435    #[test]
436    fn test_typed_single_owner_round_trip() {
437        let soc = sample_single_owner();
438        let address = *soc.address();
439        let any: DefaultAnyChunk = soc.into();
440
441        let encoded = any.to_typed_bytes();
442        assert_eq!(encoded[0], 1, "SINGLE_OWNER tag must be 1");
443
444        let decoded = DefaultAnyChunk::from_typed_bytes(&address, &encoded).unwrap();
445        assert!(decoded.is_single_owner());
446        assert_eq!(decoded.type_id(), ChunkTypeId::SINGLE_OWNER);
447        assert_eq!(decoded.address(), any.address());
448        assert_eq!(decoded.data(), any.data());
449    }
450
451    #[test]
452    fn test_typed_custom_round_trip() {
453        // The Custom variant has been removed; an unrecognised type tag must now
454        // error rather than round-trip as an opaque blob.
455        let type_id = ChunkTypeId::custom(200);
456        let address: ChunkAddress = [0x11u8; 32].into();
457        let data = Bytes::from_static(b"opaque custom chunk bytes");
458
459        let encoded = {
460            let mut out = Vec::with_capacity(1 + data.len());
461            out.push(type_id.as_u8());
462            out.extend_from_slice(&data);
463            out
464        };
465
466        let result = DefaultAnyChunk::from_typed_bytes(&address, &encoded);
467        assert!(
468            result.is_err(),
469            "unrecognised type tags must error now that Custom is removed",
470        );
471    }
472
473    #[test]
474    fn test_typed_dispatch_by_tag_not_trial_parse() {
475        // A CONTENT-tagged payload must decode as Content, even though the old
476        // address-disambiguating path would also attempt a SOC parse.
477        let content = DefaultContentChunk::new(&b"dispatch sanity"[..]).unwrap();
478        let address = *content.address();
479        let any: DefaultAnyChunk = content.into();
480
481        let encoded = any.to_typed_bytes();
482        let decoded = DefaultAnyChunk::from_typed_bytes(&address, &encoded).unwrap();
483
484        // Returned variant matches the tag exactly.
485        assert!(decoded.is_content());
486        assert!(!decoded.is_single_owner());
487    }
488
489    #[test]
490    fn test_typed_decode_empty_input_errors() {
491        let address: ChunkAddress = [0u8; 32].into();
492        let result = DefaultAnyChunk::from_typed_bytes(&address, &[]);
493        assert!(result.is_err(), "empty input must error, not panic");
494    }
495
496    #[test]
497    fn test_typed_decode_address_mismatch_errors() {
498        let content = DefaultContentChunk::new(&b"chunk A"[..]).unwrap();
499        let encoded = DefaultAnyChunk::from(content).to_typed_bytes();
500
501        // Decode with a deliberately wrong address.
502        let wrong: ChunkAddress = [0xFFu8; 32].into();
503        let result = DefaultAnyChunk::from_typed_bytes(&wrong, &encoded);
504        assert!(result.is_err(), "address mismatch must error");
505    }
506
507    #[test]
508    fn test_typed_decode_corrupt_content_payload_errors() {
509        let content = DefaultContentChunk::new(&b"corruptible"[..]).unwrap();
510        let address = *content.address();
511
512        // CONTENT tag but a too-short payload that cannot form a valid body.
513        let bad = vec![ChunkTypeId::CONTENT.as_u8(), 0x00];
514        let result = DefaultAnyChunk::from_typed_bytes(&address, &bad);
515        assert!(result.is_err(), "corrupt content payload must error");
516    }
517
518    #[test]
519    fn test_from_wire_bytes_content_round_trip() {
520        let content = DefaultContentChunk::new(&b"hello wire world"[..]).unwrap();
521        let address = *content.address();
522        let any: DefaultAnyChunk = content.into();
523        // Bare wire bytes carry no type tag.
524        let wire = any.clone().into_bytes();
525
526        let decoded = DefaultAnyChunk::from_wire_bytes(&address, wire.clone()).unwrap();
527        assert!(decoded.is_content());
528        assert_eq!(decoded.address(), any.address());
529        assert_eq!(decoded.into_bytes(), wire);
530    }
531
532    #[test]
533    fn test_from_wire_bytes_single_owner_round_trip() {
534        let soc = sample_single_owner();
535        let address = *soc.address();
536        let any: DefaultAnyChunk = soc.into();
537        let wire = any.clone().into_bytes();
538
539        let decoded = DefaultAnyChunk::from_wire_bytes(&address, wire.clone()).unwrap();
540        assert!(decoded.is_single_owner());
541        assert_eq!(decoded.address(), any.address());
542        assert_eq!(decoded.into_bytes(), wire);
543    }
544
545    #[test]
546    fn test_from_wire_bytes_address_mismatch_errors() {
547        let content = DefaultContentChunk::new(&b"chunk A"[..]).unwrap();
548        let wire = DefaultAnyChunk::from(content).into_bytes();
549
550        let wrong: ChunkAddress = [0xFFu8; 32].into();
551        let result = DefaultAnyChunk::from_wire_bytes(&wrong, wire);
552        assert!(result.is_err(), "address mismatch must error, not panic");
553    }
554
555    #[test]
556    fn test_from_wire_bytes_unknown_shape_errors() {
557        // Bare wire bytes carry no type tag, so only the standard content and
558        // single-owner shapes can ever be recovered. Opaque bytes that parse as
559        // neither (for the given address) simply error.
560        let opaque = Bytes::from_static(b"opaque custom-looking payload bytes");
561        let addr: ChunkAddress = [0x11u8; 32].into();
562        assert!(DefaultAnyChunk::from_wire_bytes(&addr, opaque).is_err());
563    }
564
565    // --- transformed address (redistribution sampler) bee parity -------------
566    //
567    // nectar owns the parity oracle for the anchor-keyed transformed address.
568    // The vectors below are taken from bee so that any drift in the prefixed
569    // BMT or the single-owner outer wrap is caught here, at the primitive.
570
571    /// bee `TestSampleVectorCAC` (`pkg/storer/sample_test.go`): a 4096-byte CAC
572    /// whose payload is the repeating pattern `i % 256`, transformed under the
573    /// anchor `swarm-test-anchor-deterministic!`.
574    #[test]
575    fn transformed_address_reproduces_bee_cac_vector() {
576        use alloy_primitives::hex;
577
578        const ANCHOR: &[u8] = b"swarm-test-anchor-deterministic!";
579        // Plain (unprefixed) BMT root: the chunk's own content address.
580        const WANT_CHUNK_ADDR: &str =
581            "902406053a7a2f3a17f16097e1d0b4b6a4abeae6b84968f5503ae621f9522e16";
582        // Anchor-keyed transformed address.
583        const WANT_TRANSFORMED: &str =
584            "9dee91d1ed794460474ffc942996bd713176731db4581a3c6470fe9862905a60";
585
586        let mut payload = vec![0u8; 4096];
587        for (i, b) in payload.iter_mut().enumerate() {
588            *b = (i % 256) as u8;
589        }
590
591        let content = DefaultContentChunk::new(payload).unwrap();
592
593        // The chunk's plain BMT address is the unprefixed root.
594        assert_eq!(
595            hex::encode(content.address().as_slice()),
596            WANT_CHUNK_ADDR,
597            "plain BMT address must match bee's published vector",
598        );
599
600        let any: DefaultAnyChunk = content.into();
601        let tr = any.transformed_address(ANCHOR);
602        assert_eq!(
603            hex::encode(tr.as_slice()),
604            WANT_TRANSFORMED,
605            "CAC transformed address must match bee byte-for-byte",
606        );
607    }
608
609    /// A single-owner chunk vector from bee's `TestMakeInclusionProofsRegression`
610    /// oracle (anchor1 = `0x64`). Exercises the SOC path: the wrapped content
611    /// chunk is re-hashed under the anchor, then the SOC transformed address is
612    /// the plain `keccak256(soc_address || inner)`.
613    #[test]
614    fn transformed_address_reproduces_bee_soc_vector() {
615        use alloy_primitives::hex;
616
617        // anchor1 from the oracle, a single byte 0x64 (== 100).
618        const ANCHOR: &[u8] = &[0x64];
619        const WANT_CHUNK_ADDR: &str =
620            "71d5144d0525b82cd550aa9254245c6195fdac9ccbb625eb45a0bfe244cb131f";
621        const WANT_TRANSFORMED: &str =
622            "521f50f895dc1deea14448c09ba8d9c510c5db09cd84c7ed8413b66f15fbc110";
623        // Full SOC wire bytes: id(32) || signature(65) || span(8) || payload.
624        const SOC_WIRE: &str = "82d0ed66f956ed70445d02df922606e02c11a79014cc441f9cc678275d260703\
625            15c7157590f599a81b38834786f79f57a6a6626bbe8bf48fbbd6db3e55ad41c0\
626            277d7d310bbefbb5d81d74605a5950171229ebdd320249ef5f8fd6b8dfe2bc7d\
627            1c1a00000000000000556e73746f707061626c65206461746121204368756e6b\
628            202331";
629
630        let wire = hex::decode(SOC_WIRE.replace([' ', '\n'], "")).unwrap();
631        let soc = DefaultSingleOwnerChunk::try_from(wire.as_slice()).unwrap();
632
633        // Sanity: this SOC's own address matches the oracle.
634        assert_eq!(
635            hex::encode(soc.address().as_slice()),
636            WANT_CHUNK_ADDR,
637            "SOC address must match bee's oracle",
638        );
639
640        // `unwrap_cac` must expose the wrapped content body with no manual
641        // 32 + 65 header slicing; its span/payload feed the inner BMT.
642        let cac = soc.unwrap_cac();
643        assert_eq!(cac.span(), soc.inner_body().span());
644        assert_eq!(cac.data(), soc.inner_body().data());
645
646        let any: DefaultAnyChunk = soc.into();
647        let tr = any.transformed_address(ANCHOR);
648        assert_eq!(
649            hex::encode(tr.as_slice()),
650            WANT_TRANSFORMED,
651            "SOC transformed address must match bee byte-for-byte",
652        );
653    }
654}