Skip to main content

bee/swarm/
typed_bytes.rs

1//! Typed byte newtypes for the Swarm protocol.
2//!
3//! Mirrors bee-go's `pkg/swarm/typed_bytes.go` (which itself mirrors
4//! bee-js's `src/utils/typed-bytes.ts`). Each type is a length-validated
5//! wrapper over a fixed-size byte array; on the wire each is encoded as
6//! lowercase hex without `0x` prefix.
7
8use std::fmt;
9use std::str::FromStr;
10
11use sha3::{Digest, Keccak256};
12
13use crate::swarm::bytes::{decode_hex, encode_hex};
14use crate::swarm::errors::Error;
15
16// ---- length constants --------------------------------------------------
17
18/// Length of a plain content reference.
19pub const REFERENCE_LENGTH: usize = 32;
20/// Length of an encrypted content reference (key suffix appended).
21pub const ENCRYPTED_REFERENCE_LENGTH: usize = 64;
22/// Length of a postage batch identifier.
23pub const BATCH_ID_LENGTH: usize = 32;
24/// Length of a transaction hash.
25pub const TRANSACTION_ID_LENGTH: usize = 32;
26/// Length of a peer overlay address.
27pub const PEER_ADDRESS_LENGTH: usize = 32;
28/// Length of an arbitrary identifier (SOC / GSOC).
29pub const IDENTIFIER_LENGTH: usize = 32;
30/// Length of a feed / PSS topic.
31pub const TOPIC_LENGTH: usize = 32;
32/// Length of an Ethereum address.
33pub const ETH_ADDRESS_LENGTH: usize = 20;
34/// Length of a secp256k1 private key.
35pub const PRIVATE_KEY_LENGTH: usize = 32;
36/// Length of a secp256k1 public key (uncompressed `X || Y`).
37pub const PUBLIC_KEY_LENGTH: usize = 64;
38/// Length of an Ethereum signed-message signature (`R || S || V`).
39pub const SIGNATURE_LENGTH: usize = 65;
40/// Length of a chunk span (little-endian `u64`).
41pub const SPAN_LENGTH: usize = 8;
42/// Length of a feed index (big-endian `u64`).
43pub const FEED_INDEX_LENGTH: usize = 8;
44
45// ---- macro for fixed-length types --------------------------------------
46
47/// Define a fixed-length typed-byte newtype with hex parsing,
48/// `Display` / `Debug` / `LowerHex` / `FromStr` and serde impls.
49///
50/// Used internally to build the 12 typed wrappers; not part of the
51/// public API.
52macro_rules! define_typed_bytes {
53    (
54        $(#[$meta:meta])*
55        $name:ident, $len:expr, $kind:literal
56    ) => {
57        $(#[$meta])*
58        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
59        pub struct $name([u8; $len]);
60
61        impl $name {
62            #[doc = concat!("Length in bytes (", stringify!($len), ").")]
63            pub const LENGTH: usize = $len;
64
65            #[doc = concat!("Construct a [`", stringify!($name), "`] from bytes. Returns ")]
66            #[doc = "[`Error::LengthMismatch`] if `b.len()` does not match."]
67            pub fn new(b: &[u8]) -> Result<Self, Error> {
68                if b.len() != $len {
69                    return Err(Error::LengthMismatch {
70                        kind: $kind,
71                        expected: &[$len],
72                        got: b.len(),
73                    });
74                }
75                let mut a = [0u8; $len];
76                a.copy_from_slice(b);
77                Ok(Self(a))
78            }
79
80            /// Parse from hex (with or without `0x` prefix).
81            pub fn from_hex(s: &str) -> Result<Self, Error> {
82                Self::new(&decode_hex(s)?)
83            }
84
85            /// Borrow the raw bytes.
86            pub fn as_bytes(&self) -> &[u8] {
87                &self.0
88            }
89
90            /// Owned copy of the raw bytes.
91            pub fn to_vec(&self) -> Vec<u8> {
92                self.0.to_vec()
93            }
94
95            /// Lowercase hex, no `0x` prefix.
96            pub fn to_hex(&self) -> String {
97                encode_hex(&self.0)
98            }
99
100            /// Consume into the inner array.
101            pub fn into_array(self) -> [u8; $len] {
102                self.0
103            }
104        }
105
106        impl fmt::Debug for $name {
107            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108                write!(f, "{}({})", stringify!($name), self.to_hex())
109            }
110        }
111
112        impl fmt::Display for $name {
113            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114                f.write_str(&self.to_hex())
115            }
116        }
117
118        impl fmt::LowerHex for $name {
119            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120                f.write_str(&self.to_hex())
121            }
122        }
123
124        impl FromStr for $name {
125            type Err = Error;
126            fn from_str(s: &str) -> Result<Self, Self::Err> {
127                Self::from_hex(s)
128            }
129        }
130
131        impl serde::Serialize for $name {
132            fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
133                s.serialize_str(&self.to_hex())
134            }
135        }
136
137        impl<'de> serde::Deserialize<'de> for $name {
138            fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
139                let s = String::deserialize(d)?;
140                Self::from_hex(&s).map_err(serde::de::Error::custom)
141            }
142        }
143    };
144}
145
146// ---- Reference (32 OR 64 bytes) ----------------------------------------
147
148/// Swarm content reference. 32 bytes for a plain CAC reference, 64
149/// bytes for an encrypted reference (CAC address || encryption key).
150#[derive(Clone, PartialEq, Eq, Hash)]
151pub enum Reference {
152    /// Plain 32-byte reference (BMT root over the chunk).
153    Plain([u8; REFERENCE_LENGTH]),
154    /// Encrypted 64-byte reference (32-byte CAC addr || 32-byte key).
155    Encrypted([u8; ENCRYPTED_REFERENCE_LENGTH]),
156}
157
158impl Reference {
159    /// All accepted lengths.
160    pub const ALLOWED_LENGTHS: &'static [usize] = &[REFERENCE_LENGTH, ENCRYPTED_REFERENCE_LENGTH];
161
162    /// Construct from raw bytes. Length must be 32 or 64.
163    pub fn new(b: &[u8]) -> Result<Self, Error> {
164        match b.len() {
165            REFERENCE_LENGTH => {
166                let mut a = [0u8; REFERENCE_LENGTH];
167                a.copy_from_slice(b);
168                Ok(Reference::Plain(a))
169            }
170            ENCRYPTED_REFERENCE_LENGTH => {
171                let mut a = [0u8; ENCRYPTED_REFERENCE_LENGTH];
172                a.copy_from_slice(b);
173                Ok(Reference::Encrypted(a))
174            }
175            n => Err(Error::LengthMismatch {
176                kind: "Reference",
177                expected: Self::ALLOWED_LENGTHS,
178                got: n,
179            }),
180        }
181    }
182
183    /// Parse from hex (with or without `0x` prefix).
184    pub fn from_hex(s: &str) -> Result<Self, Error> {
185        Self::new(&decode_hex(s)?)
186    }
187
188    /// Borrow the raw bytes (32 or 64).
189    pub fn as_bytes(&self) -> &[u8] {
190        match self {
191            Reference::Plain(a) => a,
192            Reference::Encrypted(a) => a,
193        }
194    }
195
196    /// Owned copy of the raw bytes (32 or 64).
197    pub fn to_vec(&self) -> Vec<u8> {
198        self.as_bytes().to_vec()
199    }
200
201    /// Lowercase hex, no `0x` prefix.
202    pub fn to_hex(&self) -> String {
203        encode_hex(self.as_bytes())
204    }
205
206    /// True for the 64-byte encrypted variant.
207    pub fn is_encrypted(&self) -> bool {
208        matches!(self, Reference::Encrypted(_))
209    }
210
211    /// Length in bytes (32 or 64).
212    pub fn len(&self) -> usize {
213        self.as_bytes().len()
214    }
215
216    /// Whether this reference holds zero bytes — never true for a
217    /// constructed `Reference` but useful in default-value paths.
218    pub fn is_empty(&self) -> bool {
219        false
220    }
221}
222
223impl fmt::Debug for Reference {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        write!(f, "Reference({})", self.to_hex())
226    }
227}
228
229impl fmt::Display for Reference {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        f.write_str(&self.to_hex())
232    }
233}
234
235impl fmt::LowerHex for Reference {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.write_str(&self.to_hex())
238    }
239}
240
241impl FromStr for Reference {
242    type Err = Error;
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        Self::from_hex(s)
245    }
246}
247
248impl serde::Serialize for Reference {
249    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
250        s.serialize_str(&self.to_hex())
251    }
252}
253
254impl<'de> serde::Deserialize<'de> for Reference {
255    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
256        let s = String::deserialize(d)?;
257        Self::from_hex(&s).map_err(serde::de::Error::custom)
258    }
259}
260
261// ---- 32-byte identifiers (no extra behavior) ---------------------------
262
263define_typed_bytes!(
264    /// Postage batch identifier (32 bytes).
265    BatchId, BATCH_ID_LENGTH, "BatchId"
266);
267
268define_typed_bytes!(
269    /// Ethereum transaction hash (32 bytes).
270    TransactionId, TRANSACTION_ID_LENGTH, "TransactionId"
271);
272
273define_typed_bytes!(
274    /// Peer overlay address (32 bytes).
275    PeerAddress, PEER_ADDRESS_LENGTH, "PeerAddress"
276);
277
278// ---- Identifier / Topic with from_string keccak helpers ----------------
279
280define_typed_bytes!(
281    /// Arbitrary 32-byte identifier (SOC / GSOC). Use
282    /// [`Identifier::from_string`] for the keccak256-of-utf8 variant.
283    Identifier, IDENTIFIER_LENGTH, "Identifier"
284);
285
286impl Identifier {
287    /// `keccak256(utf8(s))` as an identifier. Mirrors bee-js
288    /// `Identifier.fromString`.
289    pub fn from_string(s: &str) -> Self {
290        let mut h = Keccak256::new();
291        h.update(s.as_bytes());
292        let out = h.finalize();
293        let mut a = [0u8; IDENTIFIER_LENGTH];
294        a.copy_from_slice(&out);
295        Self(a)
296    }
297}
298
299define_typed_bytes!(
300    /// Feed / PSS topic (32 bytes). Use [`Topic::from_string`] for the
301    /// keccak256-of-utf8 variant.
302    Topic, TOPIC_LENGTH, "Topic"
303);
304
305impl Topic {
306    /// `keccak256(utf8(s))` as a topic. Mirrors bee-js
307    /// `Topic.fromString`.
308    pub fn from_string(s: &str) -> Self {
309        let mut h = Keccak256::new();
310        h.update(s.as_bytes());
311        let out = h.finalize();
312        let mut a = [0u8; TOPIC_LENGTH];
313        a.copy_from_slice(&out);
314        Self(a)
315    }
316}
317
318// ---- EthAddress with EIP-55 checksum -----------------------------------
319
320define_typed_bytes!(
321    /// Ethereum address (20 bytes). [`EthAddress::to_checksum`] returns
322    /// the EIP-55 mixed-case representation with `0x` prefix.
323    EthAddress, ETH_ADDRESS_LENGTH, "EthAddress"
324);
325
326impl EthAddress {
327    /// EIP-55 checksum representation, `0x`-prefixed.
328    pub fn to_checksum(&self) -> String {
329        let lower = self.to_hex();
330        let mut h = Keccak256::new();
331        h.update(lower.as_bytes());
332        let hash = h.finalize();
333        let mut out = String::with_capacity(2 + lower.len());
334        out.push_str("0x");
335        for (i, c) in lower.chars().enumerate() {
336            if c.is_ascii_digit() {
337                out.push(c);
338            } else {
339                // Each hex char maps to 4 bits; nibble of hash[i/2].
340                let nibble = if i % 2 == 0 {
341                    hash[i / 2] >> 4
342                } else {
343                    hash[i / 2] & 0x0f
344                };
345                if nibble >= 8 {
346                    out.push(c.to_ascii_uppercase());
347                } else {
348                    out.push(c);
349                }
350            }
351        }
352        out
353    }
354}
355
356// ---- Span (8 bytes, little-endian u64) ---------------------------------
357
358define_typed_bytes!(
359    /// Chunk span: 8 bytes, little-endian `u64`.
360    Span, SPAN_LENGTH, "Span"
361);
362
363impl Span {
364    /// Encode `n` as a little-endian span.
365    pub fn from_u64(n: u64) -> Self {
366        Self(n.to_le_bytes())
367    }
368
369    /// Decode the little-endian `u64`.
370    pub fn to_u64(&self) -> u64 {
371        u64::from_le_bytes(self.0)
372    }
373}
374
375// ---- FeedIndex (8 bytes, big-endian u64) -------------------------------
376
377define_typed_bytes!(
378    /// Feed index: 8 bytes, big-endian `u64`. The all-`0xff` value is a
379    /// "before first" sentinel matching bee-js `FeedIndex.MINUS_ONE`.
380    FeedIndex, FEED_INDEX_LENGTH, "FeedIndex"
381);
382
383impl FeedIndex {
384    /// All-`0xff` "before first" / wraparound sentinel.
385    pub const MINUS_ONE: FeedIndex = FeedIndex([0xff; FEED_INDEX_LENGTH]);
386
387    /// Encode `n` as a big-endian feed index.
388    pub fn from_u64(n: u64) -> Self {
389        Self(n.to_be_bytes())
390    }
391
392    /// Decode the big-endian `u64`. Returns `u64::MAX` for `MINUS_ONE`.
393    pub fn to_u64(&self) -> u64 {
394        u64::from_be_bytes(self.0)
395    }
396
397    /// Successor index. The `MINUS_ONE` sentinel wraps to `0`.
398    pub fn next(&self) -> Self {
399        if self == &Self::MINUS_ONE {
400            Self::from_u64(0)
401        } else {
402            Self::from_u64(self.to_u64() + 1)
403        }
404    }
405}
406
407// ---- Signature (65 bytes, R || S || V) ---------------------------------
408
409define_typed_bytes!(
410    /// Ethereum signed-message signature: `R || S || V` with V
411    /// normalized to `{27, 28}` on the wire, matching bee-js.
412    Signature, SIGNATURE_LENGTH, "Signature"
413);
414
415// PrivateKey, PublicKey: defined in [`crate::swarm::keys`] so the
416// crypto-heavy parts don't bloat this file.
417
418// ---- tests -------------------------------------------------------------
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn hex_round_trip_with_and_without_0x_prefix() {
426        let hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
427        let a = BatchId::from_hex(hex).unwrap();
428        let b = BatchId::from_hex(&format!("0x{hex}")).unwrap();
429        assert_eq!(a, b);
430        assert_eq!(a.to_hex(), hex);
431    }
432
433    #[test]
434    fn fixed_length_rejects_wrong_size() {
435        assert!(BatchId::new(&[0u8; 31]).is_err());
436        assert!(BatchId::new(&[0u8; 33]).is_err());
437        assert!(BatchId::new(&[0u8; 32]).is_ok());
438    }
439
440    #[test]
441    fn reference_accepts_32_or_64() {
442        let r32 = "ab".repeat(32);
443        let r64 = "cd".repeat(64);
444        let a = Reference::from_hex(&r32).unwrap();
445        let b = Reference::from_hex(&r64).unwrap();
446        assert!(!a.is_encrypted());
447        assert!(b.is_encrypted());
448        assert_eq!(a.to_hex(), r32);
449        assert_eq!(b.to_hex(), r64);
450        assert!(Reference::from_hex(&"ab".repeat(31)).is_err());
451        assert!(Reference::from_hex(&"ab".repeat(48)).is_err());
452    }
453
454    #[test]
455    fn identifier_from_string_is_keccak256_utf8() {
456        // keccak256("hello") (Ethereum keccak, not SHA3-256)
457        let want = "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8";
458        let id = Identifier::from_string("hello");
459        assert_eq!(id.to_hex(), want);
460    }
461
462    #[test]
463    fn topic_from_string_is_keccak256_utf8() {
464        let want_a = Topic::from_string("my-topic");
465        let want_b = Topic::from_string("my-topic");
466        assert_eq!(want_a, want_b);
467        assert_ne!(want_a, Topic::from_string("other"));
468    }
469
470    #[test]
471    fn eth_address_eip55_checksum() {
472        let raw = "fb6916095ca1df60bb79ce92ce3ea74c37c5d359";
473        let addr = EthAddress::from_hex(raw).unwrap();
474        assert_eq!(
475            addr.to_checksum(),
476            "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359"
477        );
478    }
479
480    #[test]
481    fn span_round_trip_little_endian() {
482        for n in [0u64, 1, 4096, 1 << 40] {
483            let s = Span::from_u64(n);
484            assert_eq!(s.to_u64(), n);
485        }
486        // 1 → 01 00 00 00 00 00 00 00
487        assert_eq!(Span::from_u64(1).as_bytes(), &[1, 0, 0, 0, 0, 0, 0, 0]);
488    }
489
490    #[test]
491    fn feed_index_round_trip_big_endian_and_next() {
492        for n in [0u64, 1, 100, (1u64 << 32) - 1] {
493            assert_eq!(FeedIndex::from_u64(n).to_u64(), n);
494        }
495        // 1 → 00 00 00 00 00 00 00 01
496        assert_eq!(FeedIndex::from_u64(1).as_bytes(), &[0, 0, 0, 0, 0, 0, 0, 1]);
497        assert_eq!(FeedIndex::from_u64(5).next().to_u64(), 6);
498        assert_eq!(FeedIndex::MINUS_ONE.next().to_u64(), 0);
499    }
500
501    #[test]
502    fn serde_round_trip() {
503        let r = Reference::from_hex(&"ab".repeat(32)).unwrap();
504        let json = serde_json::to_string(&r).unwrap();
505        assert_eq!(json, format!("\"{}\"", "ab".repeat(32)));
506        let r2: Reference = serde_json::from_str(&json).unwrap();
507        assert_eq!(r, r2);
508    }
509
510    #[test]
511    fn from_str_works() {
512        let id: BatchId = "00".repeat(32).parse().unwrap();
513        assert_eq!(id.as_bytes(), &[0u8; 32]);
514    }
515
516    #[test]
517    fn debug_format_includes_type_name() {
518        let id = BatchId::new(&[0xab; 32]).unwrap();
519        let s = format!("{id:?}");
520        assert!(s.starts_with("BatchId("));
521        assert!(s.contains(&"ab".repeat(32)));
522    }
523}