Encrypted Contract Storage
Every contract's storage on OXN is encrypted at rest. Solidity authors write ordinary code — the encryption is applied by the runtime, transparently, on every SSTORE and SLOAD. This page explains what that means, what a node operator actually sees, and where the model has sharp edges.
Solidity authors write normal code
mapping(address => uint256) private balances;
function deposit() external payable {
balances[msg.sender] += msg.value; // just a normal SSTORE
}
function balanceOf(address who) external view returns (uint256) {
return balances[who]; // just a normal SLOAD
}
Nothing here explicitly encrypts anything. Yet on OXN, balances lives in ciphertext on disk. The runtime encrypts on every write and decrypts on every read, inside the enclave.
The developer experience: identical to Ethereum.
Key hierarchy
Each contract has a distinct storage key. The key is derived deterministically from:
- A master key held by the enclave (rotated on a schedule, backed up across validators using a distributed key management protocol)
- The contract address
The derivation is one-way — you cannot go from a contract's storage key to the master key, or from one contract's key to another's. Every contract sees a private storage namespace even though all storage shares the same underlying database.
Key rotation is automatic and does not require contract redeployment. The runtime handles re-encryption at the storage layer, invisibly to Solidity code.
What a node operator actually sees
If a node operator dumps the state trie directly, they see:
- Storage slot addresses (
keccak256(key . slot)for mappings,slotfor scalars) — in the clear - Storage values — encrypted, Deoxys-II ciphertext with per-slot nonces
They can see how much storage a contract uses, which slots are populated (i.e., how sparse or dense a mapping is), and when each slot was last written to. They cannot see what is stored in any slot.
This is important: storage layout is not confidential, storage contents are. If your slot pattern reveals structural information (e.g., "slot N was written iff user X has a non-zero balance"), that signal is public. See Confidentiality Pitfalls for guidance.
Getter functions and public storage
Solidity's public storage variables generate automatic getter functions:
mapping(address => uint256) public balances;
// generates: function balances(address) external view returns (uint256)
The storage itself is still encrypted. But the auto-generated getter is a plain view function that any caller can invoke via eth_call and read the value back. Marking storage public effectively exposes it.
For confidential storage, use private and expose access through gated view functions (which can require signed queries for authentication):
mapping(address => uint256) private balances;
function balanceOf(address who) external view returns (uint256) {
require(msg.sender == who, "not authorized");
return balances[who];
}
Explicit encrypt / decrypt (advanced)
Sometimes you need to pass encrypted data across contract boundaries — for example, storing an encrypted blob and later handing it back to another contract that must decrypt it. OXN provides precompiles for this:
Sapphire.encrypt(key, nonce, plaintext, additionalData)— Deoxys-II encryption under a caller-provided keySapphire.decrypt(key, nonce, ciphertext, additionalData)— inverse
The caller manages the key. See Precompiles: Encryption for the ABI and gas costs.
Most contracts do not need this. Standard storage encryption is sufficient for the common case of "the contract keeps its own secret". Use the explicit primitives only when you have a specific cross-contract or cross-transaction key management need.
Storage upgrades and migrations
Because storage keys are derived from the contract address, moving state to a new contract address requires explicit migration. Options:
- Copy the state through a migration transaction. The old contract exposes a gated getter that returns state (via signed queries), and the new contract stores the copied values. State passes through decrypt-and-re-encrypt inside the enclave; no plaintext ever touches the wire.
- Use a proxy pattern. Storage lives at the proxy's address; implementation contracts read and write it via
delegatecall. This preserves the storage key across implementation upgrades.
Standard OpenZeppelin proxy patterns (Transparent, UUPS) work on OXN without modification.
Next steps
- Signed Queries — authenticated reads of private storage
- Access Control Patterns — deciding who can read what
- Confidentiality Pitfalls — what leaks even when storage is encrypted