Architecture
Two layers: permissionless content, and an NFT that points at it.
BAMFS is a content-addressed, immutable filesystem that lives entirely in contract storage. Everything else follows from one split.
Two layers
The content layer is permissionless and immutable. Anyone can write bytes. Nothing is owned, nothing can be changed, and identical content deduplicates on write. A root CID resolves whether or not anybody has claimed it.
The project layer is an NFT. A project is an ABX
token, and ownerOf is the entire authorization model. Its owner publishes root
CIDs and moves tags.
Content does not need a project to exist or resolve. A project is one representation of content, and anyone can build a different one over the same bytes.
The content layer
Four contracts, each doing one thing:
| Contract | Role |
|---|---|
IStorageBackend → SSTORE2Backend | Byte-blob storage. A write deploys a contract whose bytecode is the blob. |
ContentStore | Deduplication. store(bytes) → keccak256(bytes); storing a known chunk writes nothing. |
FileStore | An ordered list of chunk hashes, plus a MIME type and a compression id. |
DirectoryStore | A lex-sorted list of (name, childCid, isDirectory) triples. |
The CIDs are plain keccak hashes behind a one-byte domain tag:
fileCid = keccak256(
0x01 || keccak256(chunkHash_0 || … || chunkHash_n)
|| keccak256(bytes(mimeType))
|| uint8(compression)
|| bytes32(outputHash)
)
entryHash_i = keccak256(keccak256(bytes(name_i)) || cid_i || bool(isDirectory_i))
directoryCid = keccak256(0x02 || keccak256(entryHash_0 || … || entryHash_n))outputHash commits to the decompressed bytes and is bytes32(0) when the
file is uncompressed — the field is always present so the preimage is uniform.
It is why two files with identical stored bytes but different codecs get
different CIDs.
Two consequences worth internalizing:
- The backend address is part of every CID. Chunk ids are
keccak256(abi.encodePacked(backend, data)), so a different backend address is a different content-address space — not a configuration preference. Use the canonical addresses. - Chunk size is a backend property, not a protocol one. The EVM backend caps at 24,575 bytes (EIP-170 minus a STOP prefix). File CIDs are computed over chunk contents, so a backend with a different cap yields the same CID for the same input bytes.
There is no protocol-level cap on directory entries — only the block gas budget
at createDirectory time.
The project layer
Project state rides on ABX PostParams:
| Param | Holds |
|---|---|
bamfs.cid | The current root CID. |
bamfs.name | A display name. Metadata only. |
bamfs.gateway | An optional preferred IPFS gateway. |
BamfsProjectHook is a configure hook — a veto that runs before any param
value persists. It is what makes the layer coherent:
- Roots are kind-locked. The first publish records whether the root is a
directory or a file, by probing the stores. Every later publish re-probes and
reverts with
RootKindLockedon a mismatch. CIDs carry no kind bit, so persisting it once lets consumers branch on a single field. - Versions are append-only. Each publish appends to a
uint32-numbered history held in hook storage, alongside a collection-wide publish ledger. - Tags are mutable named pointers.
latestis reserved and always resolves to the current root.
A second hook, BamfsProjectEnumeration, runs on transfer and maintains the
owner → tokens index. ABX collections are not ERC721Enumerable, so without it
"which projects does this address own" has no on-chain answer.
Ownership transfer is a plain ERC-721 transfer: one transaction, immediate, irreversible. It hands over the sole right to publish and to move tags.
No indexer, by construction
Every query is an eth_call against contract storage. Version history, tags,
the recent-publish feed, and ownership all live in hook storage that the hooks
maintain as writes happen.
An earlier design read these from event logs, which worked until providers pruned them — they keep a rolling window, so an index built on events expires while one built on storage does not. That correction is why the hooks carry state at all.
Companion contracts
Optional, and none of them are on the path to reading content:
IpfsCidResolverderives a canonical IPFS CIDv1 on-chain. See IPFS.IpfsCidVerifierrecords ZK-proven CID bindings. See ZK proofs.CrossChainRegistryrecords bridge-authenticated existence claims. See Cross-chain.
Determinism
- Same bytes → same file CID, across clients and languages.
- Same tree → same directory CID, given lex-sorted entries.
- Same tree → same IPFS CID, for codecs the chain can inflate.
Papers
The IPFS-derivation techniques are published as defensive publications:
