Skip to main content

bee/swarm/
soc.rs

1//! Single Owner Chunks (SOC). Mirrors bee-go's `pkg/swarm/soc.go`.
2//!
3//! A SOC is a chunk addressed by `keccak256(identifier || ownerAddress)`
4//! rather than by content hash. The owner signs `identifier || cacAddress`
5//! using the Ethereum signed-message scheme (`V ∈ {27, 28}` on the wire).
6//! Bee verifies the signature against that scheme — bee-go's first cut
7//! used raw keccak with `V ∈ {0, 1}` and every SOC upload returned
8//! 401, so this is one of the spots that *will* break against a live
9//! node if you get it wrong.
10
11use crate::swarm::bmt::{calculate_chunk_address, keccak256};
12use crate::swarm::errors::Error;
13use crate::swarm::keys::PrivateKey;
14use crate::swarm::typed_bytes::{
15    EthAddress, IDENTIFIER_LENGTH, Identifier, Reference, SIGNATURE_LENGTH, SPAN_LENGTH, Signature,
16    Span,
17};
18
19/// A single-owner chunk. The signature commits to `identifier ||
20/// cacAddress`, where `cacAddress` is the BMT address of `span ||
21/// payload`.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct SingleOwnerChunk {
24    /// SOC identifier.
25    pub identifier: Identifier,
26    /// 65-byte signature over `identifier || cacAddress`.
27    pub signature: Signature,
28    /// Recovered owner address (last 20 bytes of `keccak(pubkey)`).
29    pub owner: EthAddress,
30    /// 8-byte little-endian span.
31    pub span: Span,
32    /// Raw payload (≤ 4096 bytes).
33    pub payload: Vec<u8>,
34}
35
36impl SingleOwnerChunk {
37    /// Compute this SOC's address: `keccak256(identifier || owner)`.
38    pub fn address(&self) -> Result<Reference, Error> {
39        calculate_single_owner_chunk_address(&self.identifier, &self.owner)
40    }
41
42    /// Wire form: `identifier (32) || signature (65) || span (8) || payload`.
43    /// Suitable as the body of `POST /soc/{owner}/{id}` once `cacAddress`
44    /// is in the URL.
45    pub fn data(&self) -> Vec<u8> {
46        let mut out = Vec::with_capacity(
47            IDENTIFIER_LENGTH + SIGNATURE_LENGTH + SPAN_LENGTH + self.payload.len(),
48        );
49        out.extend_from_slice(self.identifier.as_bytes());
50        out.extend_from_slice(self.signature.as_bytes());
51        out.extend_from_slice(self.span.as_bytes());
52        out.extend_from_slice(&self.payload);
53        out
54    }
55}
56
57/// Compute the SOC address for a given identifier and owner —
58/// `keccak256(identifier || owner)`. Mirrors bee-js
59/// `calculateSingleOwnerChunkAddress`.
60pub fn calculate_single_owner_chunk_address(
61    identifier: &Identifier,
62    owner: &EthAddress,
63) -> Result<Reference, Error> {
64    let mut buf = Vec::with_capacity(identifier.as_bytes().len() + owner.as_bytes().len());
65    buf.extend_from_slice(identifier.as_bytes());
66    buf.extend_from_slice(owner.as_bytes());
67    let addr = keccak256(&buf);
68    Reference::new(&addr)
69}
70
71/// Build a SOC for `(identifier, payload)`, signed by `signer`. Mirrors
72/// bee-js `makeSingleOwnerChunk` and bee-go `CreateSOC` /
73/// `MakeSingleOwnerChunk`.
74pub fn make_single_owner_chunk(
75    identifier: &Identifier,
76    payload: &[u8],
77    signer: &PrivateKey,
78) -> Result<SingleOwnerChunk, Error> {
79    if payload.len() > crate::swarm::bmt::MAX_PAYLOAD_SIZE {
80        return Err(Error::argument(format!(
81            "SOC payload too large: {}",
82            payload.len()
83        )));
84    }
85    let span = Span::from_u64(payload.len() as u64);
86
87    // Compute CAC address over span || payload.
88    let mut full = Vec::with_capacity(SPAN_LENGTH + payload.len());
89    full.extend_from_slice(span.as_bytes());
90    full.extend_from_slice(payload);
91    let cac_addr = calculate_chunk_address(&full)?;
92
93    // Sign identifier || cacAddress.
94    let mut to_sign = Vec::with_capacity(IDENTIFIER_LENGTH + 32);
95    to_sign.extend_from_slice(identifier.as_bytes());
96    to_sign.extend_from_slice(&cac_addr);
97    let signature = signer.sign(&to_sign)?;
98
99    let owner = signer.public_key()?.address();
100
101    Ok(SingleOwnerChunk {
102        identifier: *identifier,
103        signature,
104        owner,
105        span,
106        payload: payload.to_vec(),
107    })
108}
109
110/// Parse the wire form of a SOC chunk
111/// (`identifier(32) || signature(65) || span(8) || payload(≤4096)`)
112/// and verify it matches `expected_address`. Recovers the owner from
113/// the signature using the eth-signed-message scheme.
114pub fn unmarshal_single_owner_chunk(
115    data: &[u8],
116    expected_address: &Reference,
117) -> Result<SingleOwnerChunk, Error> {
118    let min_len = IDENTIFIER_LENGTH + SIGNATURE_LENGTH + SPAN_LENGTH;
119    if data.len() < min_len {
120        return Err(Error::argument(format!(
121            "SOC data too short: {}",
122            data.len()
123        )));
124    }
125
126    let identifier = Identifier::new(&data[..IDENTIFIER_LENGTH])?;
127    let sig_start = IDENTIFIER_LENGTH;
128    let span_start = sig_start + SIGNATURE_LENGTH;
129    let payload_start = span_start + SPAN_LENGTH;
130
131    let signature = Signature::new(&data[sig_start..span_start])?;
132    let span = Span::new(&data[span_start..payload_start])?;
133    let payload = data[payload_start..].to_vec();
134
135    // Recompute cac address over span || payload.
136    let mut cac_data = Vec::with_capacity(SPAN_LENGTH + payload.len());
137    cac_data.extend_from_slice(span.as_bytes());
138    cac_data.extend_from_slice(&payload);
139    let cac_addr = calculate_chunk_address(&cac_data)?;
140
141    // Recover owner from signature over identifier || cacAddress.
142    let mut signed = Vec::with_capacity(IDENTIFIER_LENGTH + 32);
143    signed.extend_from_slice(identifier.as_bytes());
144    signed.extend_from_slice(&cac_addr);
145    let pub_key = signature.recover_public_key(&signed)?;
146    let owner = pub_key.address();
147
148    // Verify keccak256(id || owner) == expected.
149    let mut soc_input = Vec::with_capacity(IDENTIFIER_LENGTH + owner.as_bytes().len());
150    soc_input.extend_from_slice(identifier.as_bytes());
151    soc_input.extend_from_slice(owner.as_bytes());
152    let computed = keccak256(&soc_input);
153    if computed != expected_address.as_bytes() {
154        return Err(Error::argument("SOC data does not match expected address"));
155    }
156
157    Ok(SingleOwnerChunk {
158        identifier,
159        signature,
160        owner,
161        span,
162        payload,
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::swarm::typed_bytes::PRIVATE_KEY_LENGTH;
170
171    fn signer(byte: u8) -> PrivateKey {
172        PrivateKey::new(&[byte; PRIVATE_KEY_LENGTH]).unwrap()
173    }
174
175    #[test]
176    fn make_and_unmarshal_round_trip() {
177        let id = Identifier::from_string("test-identifier");
178        let pk = signer(0x55);
179        let payload = b"single owner chunk payload";
180
181        let soc = make_single_owner_chunk(&id, payload, &pk).unwrap();
182
183        // Owner must match signer's address.
184        assert_eq!(soc.owner, pk.public_key().unwrap().address());
185        // V byte is normalized to {27, 28}.
186        let v = soc.signature.as_bytes()[64];
187        assert!(v == 27 || v == 28);
188
189        let address = soc.address().unwrap();
190        let parsed = unmarshal_single_owner_chunk(&soc.data(), &address).unwrap();
191        assert_eq!(parsed.identifier, soc.identifier);
192        assert_eq!(parsed.signature, soc.signature);
193        assert_eq!(parsed.owner, soc.owner);
194        assert_eq!(parsed.span, soc.span);
195        assert_eq!(parsed.payload, soc.payload);
196    }
197
198    #[test]
199    fn unmarshal_rejects_wrong_address() {
200        let id = Identifier::from_string("id");
201        let pk = signer(0x66);
202        let soc = make_single_owner_chunk(&id, b"x", &pk).unwrap();
203
204        let wrong = Reference::new(&[0u8; 32]).unwrap();
205        assert!(unmarshal_single_owner_chunk(&soc.data(), &wrong).is_err());
206    }
207
208    #[test]
209    fn unmarshal_rejects_short_data() {
210        let any = Reference::new(&[0u8; 32]).unwrap();
211        assert!(unmarshal_single_owner_chunk(&[0u8; 10], &any).is_err());
212    }
213
214    #[test]
215    fn calculate_address_is_keccak_id_owner() {
216        let id = Identifier::from_string("hello");
217        let owner = EthAddress::from_hex("fb6916095ca1df60bb79ce92ce3ea74c37c5d359").unwrap();
218        let addr = calculate_single_owner_chunk_address(&id, &owner).unwrap();
219
220        let mut input = Vec::new();
221        input.extend_from_slice(id.as_bytes());
222        input.extend_from_slice(owner.as_bytes());
223        let want = crate::swarm::bmt::keccak256(&input);
224        assert_eq!(addr.as_bytes(), want);
225    }
226}