xOne stores XDC chain data in two layers. A small mutable store holds state and indexes. Append-only segment files hold headers, bodies and receipts. xOneRS and xOneC use libmdbx for the first layer, and xOneTS and xOnePy use SQLite.

All four xOne clients use the same layout. Only the engine under the first layer differs. The xOneTS, xOnePy and xOneC repositories are private, as in the earlier posts. For the sync side, see the sync modes post. For the operator view, see the storage documentation.

The design is ADR-005 in the xOneRS repository. Its status is proposed: the owner has not signed the acceptance record. The three other clients carry addenda that copy its rules. This post does not cover hardware sizing.

Which database engine does each xOne client use?

xOneRS and xOneC use libmdbx. xOneTS uses bun:sqlite and xOnePy uses the stdlib sqlite3. Each commit is durable, with write-ahead logging (WAL) and full sync on the SQLite engines.

REFERENCE TABLEScroll to explore all columns →
Client Hot store Durability setting
xOneRS (Rust) libmdbx (Rust crate libmdbx 0.6.6), file chain.mdbx SyncMode::Durable, WRITEMAP off
xOneC (C++) libmdbx v0.13.12 C API MDBX_SYNC_DURABLE, WRITEMAP off
xOneTS (TypeScript, Bun) bun:sqlite, file chain.sqlite WAL, synchronous=FULL
xOnePy (Python) stdlib sqlite3, file chain.sqlite WAL, synchronous=FULL, own BEGIN and COMMIT

The decision record 0135 compares the engines. It is also proposed. Every engine costs about one device flush per durable commit in the C++, TypeScript and Python spikes. The spikes ran on different hosts and loads, so they are not a ranking.

How does xOne key state?

xOne keys state by hash, as in geth's snapshot layer. The tables are HashedAccounts, HashedStorages and Bytecodes. The state root does not change.

The keys are keccak(address) in HashedAccounts and keccak(address) || keccak(slot) in HashedStorages. A Preimages table is optional, so any snap/1 peer can be a source for snap sync.

  • xOneRS: the trie is in memory by default. With --state.disk the trie nodes are rows in TrieNodes, keyed by position (disk state).
  • xOneTS: the same layout with --state.disk. An old plain-keyed datadir migrates once, in one SQLite transaction (hashed-key state).
  • xOnePy: Meta[statekeys]=hashed-v1 marks the layout. Trie nodes are rows by path (hashed-key snap).
  • xOneC: TrieNodes is keyed by hash and nothing is deleted. The store grows with every block. A datadir with address-keyed state is not migrated; the node refuses it and asks for a resync (C++ addendum).

Changesets store the whole prior record of each key, keyed by block number first. An unwind deletes keys above the target and restores the prior records. Every client keeps 4,096 blocks of changesets. A snap client needs the state of gap blocks up to 3,149 blocks below its pivot (ADR-005 section 16).

What is in the xOne datadir?

The datadir holds one hot-store file, three segment directories, a journal and a lock. xOneRS keeps its files under <datadir>/xone, and --datadir.ancient moves the segments to another disk.

The xOneRS layout:

chain.mdbx          state, changesets, indexes, Meta
static/headers/     one .seg file per 100,000 blocks
static/bodies/
static/receipts/
journal/intent      the block being imported
LOCK                exclusive lock

The segment file name is the first block of its chunk, as 10 digits, for example 0000000000.seg. A frame holds one block, with a length, the block number, the payload and a CRC-32C. On builds from main after 2026-11-15, a file also rotates before it passes --snapshot.chunk-bytes (default 2 GB), closed chunks can be rewritten zstd-compressed with xone db compact-history, and new receipts are stored without their blooms; see smaller and safer datadirs. --datadir.ancient moves static/ to another disk (flags). --db.max-size (default 1TB) and --db.growth-step (default 64MB) set the MDBX geometry (db-geometry).

The other three clients use the same static/{headers,bodies,receipts}/ names, 100,000 blocks per file, a journal directory and a LOCK file. Archive mode (--gcmode archive) adds history tables, and the datadir keeps the mode.

How do segments and publication work?

A segment is valid only up to the length in the manifest. The committed manifest is the only publication marker.

The manifest is a 257-byte record stored in the hot store, inside the same transaction as the block. Bytes beyond that length are staged and invisible to readers.

Pruning removes whole chunk files below a chunk-aligned floor. The retention window comes from the chain rules, not from a constant. For Apothem at pivot 84,000,000 the windows are 4,500 headers, 1,799 bodies and 3,149 gap snapshots (retention).

How does xOne survive a crash during a block commit?

xOne writes a journal, appends to the segments, and then commits the indexes and the manifest in one store transaction. A head is never served before that commit returns.

Order of one block:

  1. Write and fsync the journal, then rename it into place.
  2. Execute the block.
  3. Append a frame to each of the three segments.
  4. Fsync the segment files, then their directories.
  5. In one store transaction, write the indexes, the new generation and the new manifest. Commit.
  6. Publish the head in memory. Delete the journal.

A head is never served before step 5 returns. On start, a pure function compares the generation and the manifest with what the segment files hold. Segment bytes ahead of the manifest are cut. A segment shorter than the manifest, or a damaged one, makes the node refuse to start. The journal is not replayed; the block is fetched again (ADR-005 sections 5 and 7).

A failed fsync or commit stops the process. The node writes FSYNC_FAILED with the boot id, and a restart in the same boot refuses to start. The reason: after a failed write-back a retried fsync can report success while data is lost.

A node that does not sign may batch commits. xOneRS syncs once a second. xOneC syncs every 128 blocks or after one second. xOnePy batches up to 128 blocks. xOneTS defaults to at most 100. A power loss then costs the open batch, which the node fetches again. A node that signs commits each block durably.

Fsync calls per block, as each repository measured them: 10.1 in xOneTS and 7.00 in xOneC. The ADR estimates about 8 for xOneRS and says this is not measured.

How big is an xOne datadir?

The repositories record datadirs from 1.7 GB (xOneRS on Apothem) to 42 GB (xOneC on Apothem snap). The sizes depend on the network, the height and the mode, so read the table by row.

REFERENCE TABLEScroll to explore all columns →
Client Node Height Datadir Record
xOneRS net2040 archive, 2026-11-14 832,598 22.8 GB (chain.mdbx 5.03 GB) archive
xOneRS net2040 full node, run of 2026-11-12 766,981 20 GB (mdbx.dat 2.9 GB) apothem-follow
xOneRS Apothem, run of 2026-11-12 1,066,000 1.7 GB same page
xOneC net2040 archive 820,698 20.7 GiB (libmdbx 4.4 GiB, segments 16 GiB) xOneC#80, merged 2026-11-13
xOneC Apothem snap, run of 2026-11-13 pivot 85,345,615 42 GB apothem-snap
xOneTS net2040 archive, run of 2026-11-13 20,902 2.26 GB sync modes
xOnePy net2040 archive, run from 2026-11-13 46,458 14 GB sync modes

The net2040 figures include a dense load-test range, with up to 20,000 transactions per block. Do not compute a size per block from them. The repositories do not publish a TypeScript or Python size at the chain tip yet.

How do other clients describe storage?

Other clients document their storage engines in their own terms. The table lists each claim as the client's page states it, next to the xOne fact. The sizes are for those clients' own chains, so do not compare them with the xOne datadir sizes above.

REFERENCE TABLEScroll to explore all columns →
Client What its documentation says xOne on XDC
geth Path scheme with --state.scheme=path. Trie nodes are keyed by path. 128 in-memory diff layers. Pebble engine. As stated in Geth v1.13.0. The freezer moves with --datadir.ancient, as stated in Geth databases. State is keyed by hash. --datadir.ancient moves the static/ segments to another disk.
reth Storage V2: MDBX holds hashed state only, changesets are in static files and indices are in RocksDB. --datadir.static-files puts static files on a separate drive. As stated in reth storage and Releasing reth 2.0, 2026-04-08. xOneRS and xOneC use libmdbx for state and indexes, and segment files for headers, bodies and receipts.
erigon Mainnet chaindata went from about 80 GB to about 20 GB in 3.4. As stated in Erigon 3.4, 2026-04-28. The sizes in the table above are net2040 and Apothem datadirs. They are not comparable.
besu Bonsai is the default: --data-storage-format=BONSAI. A Bonsai snap full node takes about 1.14 TB. As stated in Besu data storage formats. The engine is libmdbx or SQLite, by client.
nethermind Flat DB layouts: Flat, PreimageFlat and FlatInTrie. Mainnet takes about 260 GB. As stated in Flat DB in Nethermind, 2026-04-07. Preimages is an optional table, so any snap/1 peer can be a source for snap sync.
ethrex RocksDB is the default backend. It has a schema version and forward-only migrations. As stated in ethrex databases. xOneTS migrates an old plain-keyed datadir once, in one SQLite transaction. xOneC does not migrate an address-keyed datadir. It refuses it and asks for a resync.

Frequently asked questions

Which database does xOne use?

xOneRS and xOneC use libmdbx. xOneTS uses bun:sqlite and xOnePy uses the stdlib sqlite3. All four keep headers, bodies and receipts in append-only segment files.

Where does xOne keep chain data on disk?

xOneRS keeps chain.mdbx, the static/headers, static/bodies and static/receipts directories, a journal and a LOCK file under <datadir>/xone. The other three clients use the same static/ names, 100,000 blocks per file, a journal directory and a LOCK file.

Can I put the xOne segment files on another disk?

Yes. In xOneRS, --datadir.ancient moves static/ to another disk.

What happens to xOne if the power fails during a block commit?

On start, a pure function compares the generation and the manifest with the segment files. Segment bytes ahead of the manifest are cut, and the node fetches the block again. A segment shorter than the manifest, or a damaged one, makes the node refuse to start.

Does xOne keep the state of old blocks?

Changesets keep the whole prior record of each key. Every client keeps 4,096 blocks of changesets. Archive mode (--gcmode archive) adds history tables, and the archive mode post covers it.

Sources