Bee version: 2.8.1-7cf53193 (API 8.1.0), single node, default config.
Summary
Uploading a single-owner chunk to an address that already holds a SOC returns
201 Created, but the stored chunk is unchanged. The new payload is discarded
with no error, no warning, and a success status code. A client has no way to
tell a write that landed from a write that was dropped.
This happens on a mutable batch (immutableFlag: false), which is the case
where a caller is most likely to expect the write to take effect.
Reproduction
import requests
from eth_hash.auto import keccak
from eth_keys import keys
BEE = "http://localhost:1633"
BATCH = "<a usable batch with immutableFlag: false>"
pk = keys.PrivateKey(bytes.fromhex("11" * 32))
owner = pk.public_key.to_canonical_address()
ident = keccak(b"overwrite-demo") # fixed id -> fixed address
addr = keccak(ident + owner)
def bmt_root(payload): # BMT over 32-byte segments
lvl = [payload[i:i+32].ljust(32, b"\0") for i in range(0, 4096, 32)]
while len(lvl) > 1:
lvl = [keccak(lvl[i] + lvl[i+1]) for i in range(0, len(lvl), 2)]
return lvl[0]
def put(payload):
cac = len(payload).to_bytes(8, "little") + payload
digest = keccak(ident + keccak(cac[:8] + bmt_root(cac[8:])))
sig = pk.sign_msg_hash(keccak(b"\x19Ethereum Signed Message:\n32" + digest))
sig_b = sig.r.to_bytes(32,"big") + sig.s.to_bytes(32,"big") + bytes([sig.v + 27])
return requests.post(f"{BEE}/soc/{owner.hex()}/{ident.hex()}",
params={"sig": sig_b.hex()}, data=cac,
headers={"swarm-postage-batch-id": BATCH,
"content-type": "application/octet-stream"})
for v in (b"VERSION-ONE", b"VERSION-TWO", b"VERSION-THREE"):
st = put(v).status_code
got = requests.get(f"{BEE}/chunks/{addr.hex()}").content[105:105+len(v)]
print(f"wrote {v.decode():<15} POST={st} GET -> {got!r}")
Actual
wrote VERSION-ONE POST=201 GET -> b'VERSION-ONE'
wrote VERSION-TWO POST=201 GET -> b'VERSION-ONE'
wrote VERSION-THREE POST=201 GET -> b'VERSION-ONE'
All three uploads are signed correctly by the owner and all three are accepted
with 201. Only the first is stored.
Expected
Either of these would be fine; the current behaviour is neither:
- Reject explicitly —
409 Conflict (or 403), with a body explaining that
a SOC already exists at this address and replacement is not supported. The
caller learns the write did not happen.
- Honour the overwrite — replace the stored chunk, since the signature
proves owner intent. 200 OK rather than 201 Created.
The problem is not which of these Bee chooses. It is that 201 Created asserts
a write that did not occur.
Why it matters
Any application treating SOC as a keyed store — a fixed address per logical
slot, updated over time — will appear to work in testing (the first write
lands) and silently serve stale data forever after. The failure is invisible at
the point of the bug and only surfaces much later as data corruption.
Feeds sidestep this by deriving a fresh identifier per update, so the mainline
Bee use case never hits it. That is exactly why it survives unnoticed.
Open questions
- Is the discard happening in the localstore dedup path, or in SOC validation?
- Would a node in the target neighbourhood accept the replacement, making the
outcome depend on whether the uploader happens to be near the address? If so
the behaviour is not just unreported but nondeterministic.
- Is "SOC is write-once per address" an intended invariant? If so it should be
documented alongside the /soc endpoint, which currently does not say it.
Bee version: 2.8.1-7cf53193 (API 8.1.0), single node, default config.
Summary
Uploading a single-owner chunk to an address that already holds a SOC returns
201 Created, but the stored chunk is unchanged. The new payload is discardedwith no error, no warning, and a success status code. A client has no way to
tell a write that landed from a write that was dropped.
This happens on a mutable batch (
immutableFlag: false), which is the casewhere a caller is most likely to expect the write to take effect.
Reproduction
Actual
All three uploads are signed correctly by the owner and all three are accepted
with
201. Only the first is stored.Expected
Either of these would be fine; the current behaviour is neither:
409 Conflict(or403), with a body explaining thata SOC already exists at this address and replacement is not supported. The
caller learns the write did not happen.
proves owner intent.
200 OKrather than201 Created.The problem is not which of these Bee chooses. It is that
201 Createdassertsa write that did not occur.
Why it matters
Any application treating SOC as a keyed store — a fixed address per logical
slot, updated over time — will appear to work in testing (the first write
lands) and silently serve stale data forever after. The failure is invisible at
the point of the bug and only surfaces much later as data corruption.
Feeds sidestep this by deriving a fresh identifier per update, so the mainline
Bee use case never hits it. That is exactly why it survives unnoticed.
Open questions
outcome depend on whether the uploader happens to be near the address? If so
the behaviour is not just unreported but nondeterministic.
documented alongside the
/socendpoint, which currently does not say it.