Written by Ananya Appan, James Austgen, Kushal Babel and Sourav Das
How do you read blockchain state from an RPC node without revealing what you read? We introduce write-public ORAM, a new and practical oblivious RAM especially suited for blockchains.
This post is based on our paper Write-Public Oblivious RAM and Its Applications. Most of the work was performed during our 2026 research internship program, with Ananya and James as research interns.
A Private RPC Node
Most blockchain users do not have the resources to run their own blockchain nodes and rely on third-party RPC servers to make queries about the blockchain state. However, these queries are sensitive: they often contain addresses and other parameters that leak the precise identity and activity of the user. For example, wallets commonly simulate a transaction using RPC prior to signing and broadcasting it; these queries could be used to predict the transaction a user is about to make, leading to front-running attacks. The RPC node also learns the IP address of the sender and when the sender is active. Perhaps more surprisingly, simply displaying balances requires querying the RPC for every address a user controls, which links those addresses to a single entity and enables cross-account tracking.
Currently, users must trust RPC providers not to log or act on the sensitive requests they make. As a result, RPC server listings such as Chainlist publish the stated privacy policy and data retention practices of each provider.

However, there is no assurance that RPC providers actually follow their privacy policies. Instead of trusting their policies, it would be safer to trust in technology that enforces privacy. For this reason, the community has been interested in designing algorithms that allow users to perform RPC queries while hiding the data that they are interested in. While this seems magical, it is possible using modern-day cryptography! In this blog post, we will look at how we can improve some of this cryptography to achieve a more performant private RPC design.
Prior Approaches
One approach that enables private querying is a cryptographic primitive called Private Information Retrieval (PIR). The idea is simple: to use PIR to retrieve, for example, an account's balance, the user sends a special encryption of the account to the RPC server. The server performs operations on its database that compute and return an encryption of the account's balance. The catch is that, unless the user (or server) performs some expensive pre-processing, the server has to read each entry of the database for each query that the user makes. Otherwise, the entries it skips could reveal which parts of the database the user is interested in. This cost is acceptable for small databases but is impractical for a blockchain database containing millions of accounts.
Another approach for private RPC design relies on Trusted Execution Environments (TEEs). TEEs operate within secure hardware such that even the operating system cannot observe or tamper with applications running inside them. However, TEEs do not have sufficient memory to store the entire blockchain state and must rely on external storage devices that reside outside the secure enclave. An eavesdropper snooping on the external storage would be able to tell which accounts are being queried if a standard blockchain node is simply placed inside a TEE.

One might think that this could be fixed by encrypting the data that is stored on external storage, but this still allows the eavesdropper to learn the frequency with which accounts are accessed. The eavesdropper can correlate this frequency with public information, such as state changes from new blocks or simply the popularity of a storage slot, allowing them to learn which account is being queried with surprising accuracy.
In order to hide access patterns, the best approach would be to use a TEE with another cryptographic primitive called Oblivious RAM (ORAM). ORAM allows storage to be queried while leaking only the number of queries. It hides everything else, including whether a query reads or updates data and which data entries are read or updated. Using ORAM, the RPC node running inside the TEE accesses seemingly random locations in the external storage but is still able to receive an encryption of the balance. While in practice, there have been side channel exploits on confidentiality of TEEs (e.g., sgx.fail, tee.fail), the focus of this work is on hiding the access pattern emanating even from an ideal TEE.

ORAM is also much more efficient than PIR; in fact, it is efficient enough to be used in practice. For instance, Path ORAM, which is the most efficient ORAM in practice, is being used by Signal to enable private contact discovery. ORAM outperforms PIR because it leverages the TEE, which enables the (encrypted) data in storage to be shuffled according to a hidden permutation known only to the application running inside the TEE, removing the need to scan each database entry.
A New ORAM for Private RPC
Notice that ORAM gives us more than what is needed to design a private RPC node. In particular, ORAM also conceals what content is written to external storage. However, blockchain data is public, and there is no need to hide this information. For a private RPC node, we only need to conceal the content that is fetched.
What suffices instead is a notion of ORAM that leaks what is written to storage, but hides what is retrieved. Since this is a weaker notion, such a primitive could possibly be more efficient than standard ORAM. We searched the literature but could not find any primitive of this sort. So we decided to propose this as a new type of ORAM, and see if we could build one that is more efficient than full-fledged ORAM.
We call this a write-public ORAM. In our paper, we design a write-public ORAM that writes to storage at a cost, for blockchain workloads, that is 3–4 times cheaper than Path ORAM, with reads being only slightly (about 1.3 times) worse than Path ORAM! In the rest of the blog post, we outline how we achieve this improvement.
Path ORAM
Our write-public ORAM builds on top of Path ORAM, and so we review how Path ORAM works. We will soon see how seemingly random numbers can be used to retrieve an account's balance!
Path ORAM organizes server storage as a complete binary tree; the number of leaves in the tree is proportional to the number of data items stored in external storage. Path ORAM maps each data item to a uniformly random leaf of the tree and maintains the invariant that the (encrypted) item lies somewhere on the path to the mapped leaf. The mapping from data items to leaves is a secret known only to the RPC node running inside the TEE. For now, let us assume that this mapping is simply stored in TEE memory (we will later see how this can be stored in an efficient way).

In the above example, data items A, B, C, and D are mapped to leaves 3, 4, 1, and 2, respectively. This means that they will be stored somewhere on the paths to these leaves. Nodes of the tree that do not store data items store encryptions of "dummy" items that are used to hide where "real" items are present in the tree. In practice, each node of the tree stores a constant number of data items, and not just 1.
To fetch a data item, say B, the RPC node looks up the mapping in its memory to see which leaf B is mapped to. In our example, it notices that this leaf is 4, and so, sends 4 to the external storage device, which returns the contents of the entire encrypted path to leaf 4.

The RPC node decrypts this path in local memory, and reads each node to see which one corresponds to data item B. If this data item is an account, the RPC node can then learn the balance that corresponds to this account.
Since the server saw that the path to leaf 4 was read, leaf 4 is no longer accessed "uniformly at random". If the RPC node uses leaf 4 in a later query to retrieve data item B, this query leaks information about the frequency and hence the actual data item being looked up. To work around this, the RPC node remaps B to a new uniformly random leaf and updates the mapping in memory to reflect this.
The RPC node also rearranges the downloaded path (to leaf 4) to push data items as close to their mapped leaves as possible. This process is called eviction, and it frees up nodes at the top of the tree, allowing them to hold encryptions of data items that were accessed recently. The RPC node returns an encryption of the evicted path to the server. The server replaces nodes on the path to leaf 4 with these new nodes.

In the above example, B is remapped to leaf 2 and data item A is pushed downward, toward leaf 3. A, however, cannot be pushed further since A must remain on the path to leaf 3.
The same sequence of steps can also be used to update a data item. Instead of writing back an encryption of the same data item, the RPC node writes back an encryption of the updated data item.
One central problem remains: when the number of data items N is large, the TEE cannot store the mapping between data items and leaves in its limited memory. Path ORAM circumvents this by implementing this mapping recursively using a smaller ORAM of half the size! When the recursion unfolds, the mapping is stored in ORAMs of size N/2, N/4, N/8, and so on, until the smallest ORAM is stored locally on the client, which ultimately keeps the mapping hidden from the untrusted domain.

We skip the details of exactly how the recursive map works since they are a bit involved, and refer the interested reader to the original ORAM papers. What is important to note is that this recursion incurs an overhead of O(log N) in bandwidth and number of round trips.
Write-Public ORAM
When writes are public, we show that it is possible to update a data item without incurring this O(log N) overhead! Notice that the recursive map is implemented this way mainly to hide which data item is being updated. When the writes are public, we can instead use a "non-recursive" (non-oblivious) map to directly access the leaf position for the (public) data item! A write would now look something like this:

(1) The RPC node looks up the (public) item B's leaf position L in the non-recursive map. This reveals which item is being written, but L itself is stored encrypted, so the node operator doesn't learn it. (2) The node then writes the encrypted updated item, tagged with L, to the root of the tree in the external storage.
For oblivious read requests, we will still need to hide the index during reads, so reads will continue to use the recursive map and will incur the O(log N) overhead.
There remain some issues to be sorted out. Now that we have two maps, we will need to ensure that these maps remain coherent, i.e., they always maintain the same mapping. Otherwise, it may be possible that the latest version of an item lies on the path to some leaf L (stored in the non-oblivious position map), but a read instead reads the path to a different leaf L' (stored in the oblivious position map).
We maintain coherence between the two maps using two techniques. First, when we update a data item, we do not update the leaf mapped to the data item; this frees us from the need to update the maps while updating data items.
The mapped leaf need not be updated because a write can be done without downloading the path to the mapped leaf L: the RPC node only needs to write the updated data item, along with L, to the root of the tree. To make space for new data items at the top of the tree, the RPC node can instead download and evict a uniformly random path that is independent of L. A write would now look like this.

Why did we need to fetch L from the non-recursive map then? L needs to be written along with the updated data item to the root. This ensures that the data item moves to the right leaf when paths are evicted.
One last issue remains. When we fetch a data item during a read, we read the path to the mapped leaf. We then need to re-map the data item to a new leaf and update both maps with this new mapping. While the recursive map, which is also an ORAM, can be updated easily, the non-recursive map is hard to update without revealing which mapping (and data item) is being updated. It is important that we update the non-oblivious position map; if we do not, then the latest version of an item will be stored on the path to a leaf that has already been read, so its leaf is no longer uniformly random.
Notice that what is needed to implement the non-oblivious position map is a read-public ORAM: an ORAM that reveals data items when they are read, but not when they are updated. This is because the non-oblivious position map is read during writes, which are public, but written to during reads, which are private.
This is, in a way, the opposite of a write-public ORAM! But it turns out that this is much easier to implement; in fact, the Path ORAM tree, when used without recursion, suffices to implement this. This is because evictions allow us to write a data item to a leaf of the tree without revealing which leaf is being written to. We refer the interested reader to our paper for more details.
Evaluating Write-Public ORAM
Our write-public ORAM is not only better than Path ORAM in theory, but is also better in practice! Below, we measure our throughput, which is the number of reads and writes that we can handle per second, and compare it to Path ORAM's throughput. We measure this by varying the database size.

There is a sudden drop in throughput when the database is of size 2²⁸. This artifact is caused by the size of the database no longer fitting in our file system's cache at this point.
As the below graph shows, our write throughput advantage grows with database size, and is about 3.5 times better than Path ORAM for a database of size 2³¹ entries.

Private RPC Design
Armed with a write-public ORAM, we use this to design what we set out to achieve: a private RPC node! We implement our RPC node as an Ethereum execution client that runs inside a TEE and uses the write-public ORAM to issue oblivious requests to an external storage that stores the database.
Our database uses three separate ORAMs for different parts of Ethereum's state. One stores information about accounts, such as each address's balance, smart contract code hash, and transaction nonce. The second ORAM keeps track of storage used by smart contracts, and the third stores smart contract bytecode. We next describe how we initialize, update and read from this database.

Initializing the database: We initialize the database by streaming a snapshot of the Ethereum state from an existing, trusted peer node and then writing the state into our ORAM database using our ORAM write operation. This gives us a way to bootstrap the database without having to re-execute all historical state changes.
Serving RPC requests: After the initial state sync, the node starts serving RPC requests by issuing oblivious read requests to the database. We can serve multiple non-trivial RPC methods, including eth_getBalance, eth_getCode, eth_getTransactionCount, eth_getStorageAt, eth_call and eth_estimateGas. For further network-level privacy, users can send requests via an anonymity network like Tor.
Updating the database: Once the initial state sync is complete, the execution client needs to keep its database up to date as new blocks arrive. To do this, a trusted block import helper computes the changes introduced by each new block and passes those updates to the client. The execution client can then efficiently apply them to the database using our faster ORAM write requests.
Evaluating our Private RPC
We next evaluate the performance of our private RPC implementation. For a representative workload, we use the data from Ethereum's Hoodi testnet. We split the EVM state into three ORAMs: accounts, storage, and code. We tested four configurations of database capacities, where the relative capacities of the three trees are kept the same as the relative sizes at block height 1.5 million of Hoodi testnet (the storage tree having 16 times the number of entries in the account tree, which itself has 16 times the number of entries in code tree). Our smallest configuration, with a storage tree of size 2²⁸, can fit the first 1.5 million blocks, whereas the large configuration, with a storage tree of size 2³¹, can fit the entire Ethereum mainnet state as of this writing. The following table from our paper shows the different configurations and space they occupy on disk.

All our experiments are run on a single machine with the entire database held in external storage. Our machine has an AMD Ryzen 9 9950X3D2 16-core processor, 256 GB of memory, and the storage device is a Samsung 9100 PRO 8 TB SSD. We did not benchmark inside a TEE as the TEE overhead is much smaller in comparison to any ORAM overhead.
We are now ready to evaluate the performance of our private RPC's initial state synchronization!
We benchmark state synchronization performance on the first 1.5 million blocks.

As the above graph shows, syncing the first 1.5 million blocks worth of state of Hoodi takes about 5 hours at a storage tree capacity of 2²⁸, and just under 9 hours at 2³¹.
Once synchronized, we then benchmark applying new state updates block-by-block to test whether we can keep up with the chain. Since the amount of work varies across blocks, we measure the performance of our block import in terms of gas (standard unit of workload in EVM). At our largest database configuration, the node imports new blocks at 420 Mgas/s, and close to 700 Mgas/s for the 2²⁸ configuration. For comparison, Ethereum mainnet currently processes around 2.5 Mgas/s on average, and Monad processes around 50 Mgas/s on average, with an hourly peak of 224 Mgas/s in the last month, as of this writing. In other words, our private RPC can comfortably keep up with the fastest blockchains out there!
Finally, we measure the throughput of RPC calls. As a representative workload, we benchmark eth_call that queries the ERC-20 balance of any given address. This is representative because we envision our private RPC to be integrated into wallets in order to show users their token balances without leaking their addresses to anyone. This call touches all three trees, with two lookups in the account tree (the user and the token contract), one in the code tree (the token's bytecode), and one in the storage tree (the user's balance slot).
As the below graph shows, our private RPC implementation can serve 100–125 calls per second.

Our benchmarks with real-world workload show that the theoretical gains from our WP-ORAM algorithm translate very well to practice. As a result, our private RPC implementation can synchronize with the blockchain quickly, comfortably keep up with fast modern blockchains such as Monad, and serve RPC calls obliviously at over a hundred calls per second.
In conclusion, we put forth the concept of write-public ORAM, give a concretely and asymptotically efficient algorithm with rigorous proofs, and to our knowledge, develop the first practical implementation of a private RPC for blockchains to service oblivious requests from users. Our implementation is open source at github.com/trate3/write-public-oram-rpc. Check out the paper for the full construction!