XGR Chain — State Storage & Retention
v3.4.0 update: The current release and fork-dependent changes are specified in v3.4.0 release and activation. Interchain uses Direct-BLS message authorization and independent delivery; older checkpoint/Merkle transport descriptions do not apply to the current Interchain runtime. This does not remove the chain state/transaction tries.
Document ID: XGRCHAIN-STATE-STORAGE-RETENTION
Last updated: 2026-10-27
Audience: Node operators, validator operators, RPC operators, infrastructure engineers, auditors
Release baseline: xgr-node v3.4.0
Release commit: cbe6f8a4ae1718429a9924dcf87f0d28591d74e1
Feature introduced: xgr-node v2.1.0
Feature: State Growth Control — Online State Trie Sweeper
Node implementation: xgr-network/xgr-node
Scope: Local immutable-trie storage, historical-state retention, online pruning and recovery
1. Purpose
XGRChain stores EVM state in an immutable, content-addressed trie.
As the chain evolves, new trie nodes and contract-code entries are written while historical versions can remain physically present in the local LevelDB database.
xgr-node includes State Growth Control, implemented as an online mark-and-sweep garbage collector:
Online State Trie Sweeper
The feature was introduced in:
v2.1.0
and remains part of the current:
v3.4.0
production node.
It allows operators to retain a configurable window of recent canonical state roots and reclaim historical trie/code data that is no longer reachable from the retained state.
2. Consensus boundary
The Trie Sweeper is a local storage feature.
It does not change:
- transaction execution,
- EVM semantics,
- canonical state transitions,
- block validity,
- state roots recorded in blocks,
- receipts,
- logs,
- consensus,
- validator selection,
- staking,
- genesis,
- fork activation.
For example:
Node A
sweeper disabled
Node B
sweeper enabled
retain 10,000 blocks
Node C
sweeper enabled
retain 100,000 blocks
can all follow and validate the same canonical XGRChain.
The difference is how much historical EVM state remains locally available.
3. Block history versus state history
These are separate storage concepts.
Canonical chain history
Includes:
- block headers,
- block bodies,
- transactions,
- receipts,
- logs.
Historical EVM state
Includes historical versions of:
- accounts,
- balances,
- nonces,
- contract storage,
- contract code references,
- state tries.
The Trie Sweeper targets historical EVM trie/code storage.
It does not delete normal canonical block/receipt history.
Therefore:
old block available
does not necessarily imply:
old state root still executable/queryable
4. Retention model
Operator setting:
--trie-sweeper-retain-blocks
Default:
10,000
For every sweep:
toBlock = canonical head
and:
fromBlock =
max(
0,
toBlock + 1 - retainBlocks
)
The node loads every canonical header in:
[fromBlock, toBlock]
and collects its state root.
Duplicate state roots are de-duplicated.
5. What retention means
The configured block count defines the canonical roots that must remain traversable.
For example:
retainBlocks = 10,000
means the state roots from the most recent 10,000 canonical blocks are selected as live roots.
All trie/code data reachable from those roots is preserved.
This is not equivalent to:
delete everything older than exactly 10,000 blocks.
Historical nodes can remain because:
- immutable trie nodes are shared across state versions,
- old nodes may still be reachable from a retained root,
- recent write generations are conservatively protected,
- conservative marker state can retain additional garbage.
Therefore the setting defines a minimum retained canonical state-root window, not an exact physical age cutoff.
6. What can be deleted
The production sweeper considers only two LevelDB key classes sweepable.
Trie nodes
Raw hash-addressed keys:
32-byte hash key
Contract code
Keys of the form:
"code" + 32-byte code hash
Other LevelDB keys are skipped.
The sweeper does not perform arbitrary database-key deletion.
7. Mark-and-sweep model
One production cycle performs:
select retained canonical roots
↓
start new GC generation
↓
mark reachable account trie
↓
mark reachable storage tries
↓
mark referenced contract code
↓
scan LevelDB in bounded ranges
↓
identify unmarked candidates
↓
recheck candidates under write barrier
↓
delete unreachable trie/code data
↓
compact affected LevelDB ranges
↓
capture fresh canonical head
↓
strictly verify its state root
No deletion occurs before the mark phase has completed.
8. Account and storage traversal
For every retained root, the marker recursively traverses:
account trie
↓
account value
├── contract code hash
└── contract storage root
For contract accounts it therefore marks:
- account-trie nodes,
- storage-trie nodes,
- referenced contract code.
If referenced contract code is found, the implementation recalculates its Keccak hash and verifies it against the stored account code hash.
A mismatch causes the mark operation to fail.
9. Missing retained state is an error
During marking, a hash-linked trie node required by a retained root must exist.
If it does not:
missing trie node ...
is returned.
The sweeper does not silently reinterpret missing retained data as empty state.
This is an important integrity property.
Online concurrency safety
10. Write tracking
The immutable-trie LevelDB implementation contains a GC write barrier.
When the sweeper is enabled:
normal trie write
↓
GC barrier read lock
↓
mark written key with current generation
↓
write key to trie database
Both:
Put(...)
and batched writes are generation protected.
Contract code uses the same protected storage path.
11. GC generations
When write tracking begins:
gcGeneration = 1
Each sweep starts a new generation:
gcGeneration++
For a production sweep with generation:
G
the deletion logic preserves keys whose marker generation is:
>= G - 1
Therefore both:
current generation
and:
immediately previous generation
are protected.
12. Why the previous generation is retained
The extra generation protects the race where state is written shortly before the sweep generation changes but becomes canonical only afterward.
Conceptually:
state written in generation G-1
↓
sweep begins generation G
↓
that state becomes canonical
Without the grace generation it could appear unmarked during the new retained-root snapshot.
The previous-generation protection prevents this race from deleting newly canonical state.
13. Startup synchronization
The sweeper enables write tracking before recording its synchronization head.
Startup sequence:
initialize Trie Sweeper
↓
enable GC write tracking
↓
record current canonical head H
↓
wait until canonical head > H
↓
begin first sweep
Polling interval:
250 ms
The first sweep therefore intentionally does not run immediately after process start.
14. Why it waits for a new head
A state transition may already have been in flight when write tracking was enabled.
In IBFT such a transition can target the next canonical height.
Waiting for one canonical head advance ensures that this pre-tracking/in-flight state becomes either:
- canonical and covered by retained-root selection, or
- obsolete.
This closes the startup race before deletion begins.
Production scan path
15. SweepLive
The production server uses:
SweepLive(...)
not the older snapshot-based sweep path.
This matters operationally.
SweepLive does not hold one long-lived LevelDB snapshot across a potentially very long full-database sweep.
16. Why long-lived snapshots are avoided
A LevelDB snapshot or iterator can pin SSTables that it can still see.
During a large online cleanup this could temporarily prevent physical disk reclamation of obsolete SSTables for hours.
The production implementation instead:
- marks against the live immutable database,
- scans in bounded chunks,
- releases each iterator,
- deletes candidates,
- compacts the completed key range.
This allows disk reclamation to progress during the sweep itself.
17. Range scanning
The production sweep scans the database by first-byte ranges:
0x00
0x01
...
0xff
There are therefore:
256
top-level scan ranges.
Within each range, scanning occurs in bounded chunks.
18. Internal batching defaults
Current v3.4.0 internal defaults:
| Internal setting | Value |
|---|---|
| Delete batch | 256 keys |
| Scan batch | 4096 keys |
| Mark batch | 2048 operations |
| GC pause | 10 ms |
These are implementation details, not current operator CLI settings.
Small batches and short pauses are intended to keep normal block/state processing ahead of garbage collection.
19. Candidate deletion recheck
The scan itself does not directly delete a candidate.
Before deletion:
- scan iterator is released,
- exclusive GC write barrier is acquired,
- candidate generation marker is re-read,
- candidate is deleted only if it is still unprotected.
This closes the race where a key becomes live between:
scan
and:
delete
20. Conservative failure behavior
If trie deletion succeeds but cleanup of its temporary GC marker metadata fails, the implementation reports an error.
However, stale marker metadata is conservative.
It can cause later cycles to:
retain too much
but not to:
delete live state because of the stale marker
The marker database is therefore safety-biased toward over-retention.
Compaction
21. LevelDB compaction
The production server invokes the sweeper with:
Compact: true
After a first-byte range has been scanned, that range is compacted only when the sweep actually deleted data from it.
This helps physically reclaim LevelDB storage.
Deletion alone does not guarantee immediate filesystem shrinkage without compaction.
22. Disk usage expectations
The Trie Sweeper does not create a fixed maximum database size.
Disk can continue to grow because:
- current live state grows,
- more accounts exist,
- contracts add storage,
- new code is deployed,
- retained roots share substantial history,
- the chosen retention window is large,
- LevelDB maintains SSTables and compaction overhead,
- temporary GC metadata also consumes storage during operation.
Therefore:
retainBlocks = 10,000
does not mean:
database will stay below a specific GB size
Post-sweep verification
23. Fresh canonical-head verification
After sweep deletion and compaction, the server captures the current canonical head again.
This can be newer than:
toBlock
selected at the beginning of the sweep.
That fresh head's state root is passed to:
HashCheckerStrict(...)
24. Strict hash checker
The strict checker:
- requires hash-linked trie nodes to exist,
- recursively reconstructs the trie,
- recalculates its root,
- returns an error for missing referenced nodes.
The calculated root must equal:
head.StateRoot
If not, the cycle fails with an integrity mismatch.
25. Scope of the post-sweep check
There are two integrity layers.
During mark phase
Every configured retained root is traversed.
Missing nodes or code for those roots cause marking to fail before deletion.
After sweep
A freshly captured current canonical head is traversed again using the strict checker.
The post-sweep check does not separately re-run the strict checker for every historical retained root.
It verifies that current canonical state remained intact across the online sweep.
26. Completion log
A successful production cycle logs:
generation
fromBlock
toBlock
roots
marked
scanned
deleted
retained
skipped
duration
verifiedHead
verifiedStateRoot
A normal operator should pay particular attention to:
deleted
duration
verifiedHead
verifiedStateRoot
Configuration
27. CLI flags
--trie-sweeper
--trie-sweeper-retain-blocks
--trie-sweeper-interval
Current defaults:
| Setting | Default |
|---|---|
| Sweeper | disabled |
| Retained canonical blocks | 10,000 |
| Interval | 6h |
The defaults are defined directly by the v3.4.0 server configuration.
28. Configuration-file fields
Equivalent JSON/YAML/HCL configuration fields:
trie_sweeper: true
trie_sweeper_retain_blocks: 10000
trie_sweeper_interval: 6h
29. Validation requirements
When the feature is enabled:
retainBlocks > 0
and:
interval > 0
are required.
The sweeper additionally requires:
- persistent non-empty data directory,
- initialized canonical head,
- LevelDB-backed immutable-trie storage.
If these conditions are not met, startup of the sweeper fails.
30. Work directory
The GC work directory is:
<data-dir>/trie-gc
Marker database:
<data-dir>/trie-gc/marks
Example:
/var/lib/xgr/node/trie-gc/marks
31. Marker data is temporary
When the Trie Sweeper initializes, the implementation removes and recreates:
<data-dir>/trie-gc
The marker database is therefore disposable garbage-collection metadata.
It is not canonical blockchain state.
It should not be treated as an authoritative component of a blockchain-state backup.
The canonical trie database remains the important persistent state.
Sweep scheduling
32. First cycle
The first cycle starts after:
one canonical head advance
following write-tracking activation.
It does not wait six hours before the first sweep.
33. Following cycles
After a cycle ends, the worker starts a timer for:
--trie-sweeper-interval
Default:
6h
Therefore the cadence is:
sweep duration
+
configured interval
+
next sweep
not:
fixed every six hours from process start
34. Failed cycle behavior
If a cycle fails:
- the error is logged,
- canonical chain state is not silently changed to hide the error,
- the background worker remains alive,
- the normal interval is waited before another cycle is attempted.
Integrity failures therefore remain visible in logs.
Operator profiles
35. General bounded-history node
Example:
/opt/xgr/bin/xgrchain server \
--chain /etc/xgr/genesis.json \
--data-dir /var/lib/xgr/node \
--seal=false \
--trie-sweeper \
--trie-sweeper-retain-blocks 10000 \
--trie-sweeper-interval 6h
At the nominal two-second block target:
10,000 blocks
≈ 20,000 seconds
≈ 5h 33m
The wall-clock duration is only approximate.
36. Longer-history RPC node
Example:
--trie-sweeper-retain-blocks 100000
Nominal time window:
100,000 × 2 seconds
≈ 55h 33m
A larger retention window requires more disk and more mark work.
37. Archive-style node
For unrestricted historical EVM-state access:
Trie Sweeper disabled
Do not enable pruning on the only node intended to provide arbitrary historical state.
An archive-style node is appropriate for workloads requiring old:
eth_getBalance
eth_getTransactionCount
eth_getCode
eth_getStorageAt
eth_call
debug_trace*
against arbitrary historical heights.
38. Validator profile
The Trie Sweeper can technically run on a validator because pruning is consensus-independent.
However, validators are latency-sensitive.
Before enabling it on validators:
- validate behavior on a full/RPC node,
- ensure adequate storage IOPS,
- monitor consensus round changes,
- monitor block progression,
- monitor sweep duration,
- avoid excessively aggressive retention policies without operational evidence.
Consensus reliability takes priority over disk reclamation.
RPC implications
39. State-dependent historical RPC
Pruning can affect requests such as:
eth_getBalance
eth_getTransactionCount
eth_getCode
eth_getStorageAt
eth_call
eth_estimateGas
debug_traceCall
debug_traceTransaction
debug_traceBlockByNumber
debug_traceBlockByHash
when execution requires an old state root that has been reclaimed.
40. History that remains independent
The sweeper does not target canonical:
eth_getBlockByNumber
eth_getBlockByHash
eth_getTransactionByHash
eth_getTransactionReceipt
eth_getLogs
data merely because its corresponding historical trie state is no longer retained.
A node can therefore answer:
What happened in block N?
while being unable to answer:
What would this contract call have returned against state at block N?
41. No exact expiry guarantee
Operators should not promise clients that state will disappear exactly after:
retainBlocks
because trie nodes can remain reachable through newer roots.
The safe client contract is:
State inside the configured retained canonical-root window is intended to remain available; arbitrary state older than that window is not guaranteed.
Operational monitoring
42. Logs to monitor
Initialization:
Trie sweeper enabled
retainBlocks
interval
trackingFromBlock
workDir
Startup synchronization:
Trie sweeper write tracking synchronized
trackingFromBlock
currentBlock
Cycle start:
Trie sweeper cycle started
fromBlock
toBlock
roots
Completion:
Trie sweeper cycle completed
generation
marked
scanned
deleted
retained
skipped
duration
verifiedHead
verifiedStateRoot
Failure:
Trie sweeper cycle failed
43. System metrics to correlate
Monitor:
- free filesystem capacity,
- LevelDB directory size,
- GC marker directory size,
- disk latency,
- disk throughput,
- CPU,
- memory,
- block progression,
- synchronization state,
- peer count,
- validator round behavior if applicable,
- RPC latency,
- sweep duration.
Disk-space trends should be evaluated across multiple completed sweeps rather than immediately after enabling the feature.
44. Healthy cycle indicators
A healthy completed cycle should show:
verifiedHead
verifiedStateRoot
and continued:
canonical block progression
after completion.
The absolute number of:
deleted
entries can vary widely by chain state and prior pruning history.
A low deletion count does not itself indicate failure.
45. Disk does not shrink immediately
Possible explanations include:
- few unreachable nodes existed,
- much data is shared with retained roots,
- current live state is large,
- retention window is large,
- filesystem/LevelDB effects lag behind logical deletion,
- concurrent chain growth offsets reclaimed space.
Use trend monitoring rather than a single filesystem snapshot.
Disabling and recovery
46. Disable future pruning
Remove:
--trie-sweeper
or configure:
trie_sweeper: false
This stops future sweeps.
It does not recreate deleted state.
47. Deleted historical state cannot be toggled back on
After pruning:
disable sweeper
does not mean:
historical state restored
The node has no local inverse operation that reconstructs deleted trie nodes simply from the configuration change.
48. Recovery when historical state is needed again
Possible recovery paths include:
- rebuild/resynchronize under an archive-compatible policy,
- restore a suitable pre-pruning/archive backup,
- query another archive-style node,
- rebuild a dedicated historical-state service.
Which approach is appropriate depends on the workload and available infrastructure.
49. Recovery after integrity failure
If a sweep reports:
post-sweep trie integrity check ...
or:
post-sweep trie integrity mismatch ...
treat it as a serious storage-integrity event.
Recommended procedure:
- preserve node logs,
- avoid treating the node as authoritative,
- if it is a validator, remove it from production consensus duties if integrity cannot immediately be established,
- compare block number and head hash with trusted nodes,
- check filesystem and disk health,
- preserve relevant database files for investigation if required,
- rebuild/resynchronize from a trusted state if integrity cannot be proven.
Do not suppress the error and continue assuming historical/current trie integrity.
50. Backup considerations
The canonical trie database is persistent node state.
The directory:
trie-gc
contains temporary GC tracking metadata.
A backup/recovery strategy should therefore focus on:
- canonical chain database,
- trie database,
- node role/configuration,
- validator key material where applicable.
Validator key backups must remain protected independently of storage-pruning policy.
Upgrade classification
51. Feature history
State Growth Control was introduced in:
xgr-node v2.1.0
Current verified baseline:
xgr-node v3.4.0
The original introduction remains historical; the current implementation reference should use v3.4.0.
52. Upgrade classification
| Property | Trie Sweeper |
|---|---|
| Consensus change | No |
| EVM semantics change | No |
| State-transition change | No |
| Canonical state-root change | No |
| Transaction-format change | No |
| Genesis change | No |
| Hardfork required | No |
| Activation block required | No |
| Local storage behavior | Yes |
| Historical-state availability | Yes |
| Operator configurable | Yes |
Changing local retention does not require validator-wide coordination.
53. Operator checklist before enabling
- Run a release that supports the Trie Sweeper.
- For this documentation baseline, use
v3.4.0. - Confirm a persistent data directory.
- Confirm adequate free disk space.
- Determine required historical-state window.
- Determine whether any application needs archive RPC.
- Determine whether historical debug tracing is required.
- Do not prune the only archive node.
- Record the chosen retention setting.
- Record the sweep interval.
- Ensure storage/IO monitoring is active.
- Plan recovery before deleting historical state.
54. Operator checklist after enabling
Verify:
- sweeper initialization log,
- write-tracking synchronization,
- first cycle starts after head advancement,
- canonical head continues advancing,
- synchronization remains healthy,
- sweep completion appears,
verifiedHeadis plausible,verifiedStateRootis present,- no integrity error appears,
- disk latency remains acceptable,
- applications function inside the expected state-history window.
55. Production defaults summary
| Parameter | v3.4.0 |
|---|---|
| Sweeper enabled by default | No |
| Default retention | 10,000 blocks |
| Default interval | 6h |
| Production sweep path | SweepLive |
| Long-lived full DB snapshot | No |
| Scan ranges | 256 first-byte ranges |
| Default scan batch | 4096 |
| Default delete batch | 256 |
| Default mark batch | 2048 |
| Default GC pause | 10 ms |
| Current + previous generation protected | Yes |
| Contract code marked | Yes |
| Contract code hash checked | Yes |
| Compaction | Affected ranges |
| Post-sweep current-head verification | Strict |
| GC marker DB persistent canonical state | No |
| Historical state outside window | Not guaranteed |
| Canonical block history removed | No |
56. Design principle
XGRChain separates:
canonical state correctness
from:
historical-state retention policy
The blockchain determines the canonical state root.
The operator decides how much old EVM state must remain locally queryable.
The Online State Trie Sweeper makes that policy explicit while protecting concurrent writes, retained canonical roots and the freshly captured canonical head.
The operational contract is therefore:
retain what is required
verify what remains canonical
reclaim what is no longer reachable
without changing XGRChain consensus.