# Private Ethereum State Retrieval over UBT using Vectorized BatchPIR + Oblivious Compression ## 0. Goal and Threat Model **Goal.** A client wants to retrieve *Ethereum state values* (account fields, storage slots, contract code chunks, …) from an off‑chain UBT state provider: * without revealing **which state keys** they are interested in; and * while being able to **verify** those values against an authenticated UBT root using Merkle proofs. **Parties & assumptions** * **Client (wallet / prover):** * Knows the UBT key-derivation rules and the canonical UBT root for some block. * Holds the HE secret key for PIR. * **Server (state provider):** * Stores a flattened UBT database for one or more block heights. * Knows only the HE public key + evaluation keys. * Is *honest-but-curious*: follows the protocol, but tries to learn the queried indices. * **Blockchain:** * On-chain commitment is an MPT root. The server additionally provides an authenticated **MPT→UBT transformation proof + UBT root** so the client can trust UBT proofs. Privacy notion: the server learns nothing about which UBT locations (and hence which Ethereum accounts / slots) are being queried, beyond aggregate batch size. --- ## 1. UBT as the State Backing Store We use the Unified Binary Tree (EIP‑7864) as the canonical off‑chain state representation. ### 1.1 UBT shape and proof size * **Key–value space.** A single, unified trie over 32‑byte keys and 32‑byte values: * Keys: 31‑byte **stem** + 1‑byte **subindex**. * Values: 32‑byte payloads (or Merkleized chunks, depending on node type). * **Binary arity.** The trie is arity‑2, so for a tree with ~2²⁴ leaves: * Merkle proof depth ≈ 24. * Proof size ≈ 24 siblings × 32 bytes = 768 bytes, ignoring small framing overhead. * **SNARK‑friendliness.** No extension nodes, no RLP; just a binary Merkle tree over fixed‑width hashes. This structure is exactly what we need: short proofs (log N) and a clean mapping to a flat key–value database. ### 1.2 Flattening UBT to a PIR Database The server maintains a **flat key–value database** derived from the UBT: * **Key:** a 32‑byte *UBT location identifier* (effectively the node location), derived from: * the 31‑byte stem (= shared prefix for 256 related leaves); and * a 1‑byte subindex that identifies: * which account/header leaf or storage/code leaf, or * an internal Merkle node in the binary tree. * **Value:** the serialized node payload: * node type, children hashes, and/or the leaf’s value hash/content; * enough to reconstruct Merkle paths and leaf values. This is a standard key–value store behind PIR: indices are just logical array positions over the set of all UBT nodes. The server materializes: * a mapping from `(stem, subindex)` → **array index** `j ∈ [0, N)`; and * a fixed ordering of internal nodes and leaves so that `DB[j]` is the value used by the PIR engine. ### 1.3 Mapping Ethereum State to UBT Keys The client must deterministically derive the right *UBT keys* for each high‑level “state value” it wants. We assume EIP‑7864‑style helper functions like `get_tree_key`, `get_tree_key_for_storage_slot`, and `get_tree_key_for_code_chunk` (names for exposition). | State element | UBT key layout (logical description) | | | | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | -------------------------------------------------------------------------------------------------------------------------------- | | **Account basic data** (nonce, balance, code size, version) | Packed into a single 32‑byte value at stem `s = hash(address)` and **subindex 0** (`BASIC_DATA_LEAF_KEY`). This means opening one UBT branch suffices to read all those fields. | | | | **Storage slot `k`** | `stem = hash(address | | floor(k / 256))`, `subindex = k mod 256`. Slots in a 256‑slot window share a stem and live under a single “storage stem” branch. | | **Contract code chunk `i`** | Code is chunked into 31‑byte segments plus metadata, then placed at specific subindices via `get_tree_key_for_code_chunk(address, chunk_index)`. The **first 128 chunks** can share the account header stem to improve locality (as per UBT spec). | | | Given this, the client can compute: 1. the UBT leaf key `(stem, subindex)` for each desired state value; 2. from there, the *binary path* and the list of internal nodes needed for the Merkle proof from that leaf up to the UBT root. All of these nodes map to specific database indices `I = {i₁, …, i_ℓ}` in the flattened DB. --- ## 2. From State Values to PIR Indices ### 2.1 What the Client Actually Needs For each requested **state value** (e.g., “balance of address A” or “storage slot k of contract C”), the client needs: 1. the **leaf value** (actual 32‑byte state field or code chunk); 2. the sequence of **sibling nodes** on the path from the leaf to the UBT root; 3. the **UBT root** itself and a **MPT→UBT proof** so that the root commitment is anchored in Ethereum consensus. With (1)–(3), the client can locally recompute the root and verify: * correctness of the value, and * consistency with the on‑chain MPT root. ### 2.2 Computing the Batch of Node Indices For a batch of user‑level state values `S = {x₁, …, x_m}`: 1. For each `x_j`, derive its UBT leaf key `(stem, subindex)` using the appropriate mapping. 2. For each leaf, compute the **binary path** from leaf to root under the UBT rules (public, deterministic). 3. Collect the set of **all unique UBT nodes** needed across all paths: $\mathcal{N} = \bigcup_{j=1}^m \text{PathNodes}(x_j)$ 4. Map each node in `𝒩` to its flattened DB index using the server’s public indexing scheme (or a client‑reconstructed one if feasible). 5. The result is a batch of indices: $I = {i_1, …, i_\ell}, \quad \ell = |\mathcal{N}|$ This `I` is the input to the BatchPIR protocol. Note that multiple user‑level state values often share large parts of their path, so `ℓ` is typically much smaller than `m · log N`. --- ## 3. BatchPIR Backbone We adopt a **single‑server, RLWE‑based BatchPIR** with vectorization and oblivious compression: * **Vectorized BatchPIR** (Mughees & Ren) for efficient computation + packing of small entries. * **LSObvCompress / LSObvDecompress** (Bienstock et al.) for oblivious compression of request and response ciphertexts in the cuckoo‑hashing BatchPIR framework. ### 3.1 Cuckoo Hashing and Buckets Standard pattern: * Server splits the DB of size `N` into buckets using `w` hash functions (`w ≈ 3`), producing `B ≈ 1.5ℓ` buckets at query time, each containing a subset of DB entries. * Client maps its batch indices `I` into the same bucket space using **cuckoo hashing** (max size 1 per bucket): * each real index is placed into one of its `w` candidate buckets; * empty buckets are filled with dummy index 0. * For each non‑empty bucket, we run a **vectorized PIR** instance, but we pack many buckets into a small number of ciphertexts using vectorization. This gives: * amortized server time ≈ tens of ms per entry; * baseline response size ≈ `1.5ℓ` logical PIR responses (before vectorization/compression); ### 3.2 Vectorized PIR (compressed, high level) Vectorization leverages SIMD‑like HE slots: * Each RLWE plaintext encodes a **vector of entries**; each ciphertext handles many entries simultaneously. * Within each bucket, the server: * arranges DB entries as a 3‑dimensional hypercube; * processes the PIR query dimension by dimension with homomorphic multiplications and rotations; * ends with a ciphertext that has the requested entry in exactly one slot. Packing across buckets: * Requests from multiple buckets are packed into the same ciphertext by interleaving slots. * Similarly, bucket contents are encoded so rotations don’t create collisions. * The server merges vectorized responses from many buckets into **one ciphertext** by: 1. copying each bucket’s non‑zero slot to all slots via rotate‑and‑sum; 2. masking different slot positions for different buckets; 3. adding all masked ciphertexts together. Result: retrieving 256 entries (256‑byte entries, 1M‑entry DB) gives ≈ 19× communication overhead over raw plaintext, with amortized ~123 ms per entry on a single core. --- ## 4. Oblivious Compression: LSObvCompress + LSObvDecompress Cuckoo‑based BatchPIR introduces **dummy** queries/responses (~0.5ℓ). LSObv* eliminates the need to explicitly send encryptions for these zeros. ### 4.1 LSObvDecompress for Request Compression (Client → Server) Context: among the `1.5ℓ` logical bucket queries, only `ℓ` correspond to real UBT nodes; the others are dummy. * **Compressor:** the **client** knows the set of relevant logical bucket indices `I ⊂ [1.5ℓ]`, `|I| = ℓ`. * **Decompressor:** the **server** gets only: * a compact encoding `ĉ` (≈ `(1+ε)ℓ` ciphertexts), * and the public key; it does **not** know which positions in `[1.5ℓ]` are real vs dummy. Using LSObvDecompress: 1. Client constructs a plaintext vector of query messages (one per logical bucket), with zero entries for dummy buckets. 2. Client runs the compress algorithm (solving a small random linear system) and outputs a compact encrypted representation `ĉ` of dimension ≈ `(1+ε)ℓ`. 3. Server runs the oblivious decompression algorithm, which: * homomorphically multiplies by a random band matrix; * recovers a vector of ciphertexts `c̃` of length `1.5ℓ`, where all real bucket queries are correctly reconstructed, and dummy slots can be arbitrary encryptions. 4. Server feeds `c̃` into the usual BatchPIR pipeline as if the client had sent all `1.5ℓ` bucket queries explicitly. This saves ≈ 30% of request communication compared to sending all queries, while preserving query privacy and correctness with negligible failure probability (~2⁻⁴⁰). ### 4.2 LSObvCompress for Response Compression (Server → Client) On the response side: * The server obtains `1.5ℓ` logical PIR responses, of which only `ℓ` are non‑zero (real UBT nodes); the remaining 0.5ℓ responses encrypt zero. Using LSObvCompress: 1. Server views responses as a length‑`1.5ℓ` ciphertext vector `c̃`; it knows only `t = ℓ`, *not* which positions are non‑zero. 2. It runs LSObvCompress.ObvCompress on `c̃`: * picks a random band matrix `M` with `m = (1+ε)ℓ` rows; * performs a matrix–vector product `ĉ = M·c̃` using only homomorphic additions. 3. Client, who *does* know which logical responses are relevant (i.e., which Merkle nodes it asked for), runs Decompress: * reconstructs the subset of non‑zero plaintexts `{(i, p_i)}_{i∈I}` by solving a small linear system in the clear after decryption. Result: * Response size shrinks from `1.5ℓ` logical responses to ≈ `(1+ε)ℓ` ciphertexts, with ε ≈ 0.05 in practice, i.e. ~30% reduction. * Encoding and decoding cost is dominated by O(nλ) and O(tλ) additions (no multiplications), which is small relative to the underlying PIR work. ### 4.3 Combined with Vectorization Vectorization and LSObv* are orthogonal: * vectorization packs multiple entries per ciphertext (when entries are small); * LSObv* reduces the total number of ciphertexts required for the logical bucket layer. Combined, we get: * **request:** `⌈(1+ε)ℓ / r⌉` ciphertexts instead of `⌈1.5ℓ / r⌉`, where `r` is how many bucket queries we pack per ciphertext; * **response:** `⌈(1+ε)ℓ / d⌉` ciphertexts instead of `⌈1.5ℓ / d⌉`, where `d` is how many logical responses we pack per ciphertext. --- ## 5. End-to-End Protocol Flow Putting everything together for a single block height: ### 5.1 Setup (Server, per block height) 1. **Maintain UBT state:** * Build (or incrementally update) the binary UBT from the canonical MPT using MPT→UBT migration logic from EIP‑7864. * Compute the UBT root. 2. **Flatten:** * Assign each UBT node a stable index `j ∈ [0, N)`. * Store `DB[j] = node_value` in a key–value DB keyed by `(stem, subindex)` or the node location. 3. **Preprocess for BatchPIR:** * Hash indices into `B` buckets via Angel et al.’s framework. * Encode bucket contents for vectorized PIR (hypercube layout, slot packing). 4. **HE setup:** * Receive client HE *public key* + evaluation keys (relin/rotation). * Optional: reuse the same HE parameters across many clients, but keys are per client. ### 5.2 Query (Client → Server) For a batch of state requests `S = {x₁,…,x_m}`: 1. **Compute UBT leaves:** * For each `x_j` (e.g., `(address, storage_slot)`), derive `(stem, subindex)` using UBT mapping. 2. **Derive path node indices:** * For each leaf, compute Merkle path and corresponding node indices; deduplicate to get `I = {i₁,…, i_ℓ}`. 3. **Cuckoo hashing into buckets:** * Run client‑side cuckoo hashing over `I` to assign each index to one of `B ≈ 1.5ℓ` buckets. * Fill unused buckets with dummy index. 4. **Per-bucket index → PIR index:** * Convert DB index to “index within bucket” using precomputed mapping rules. 5. **Vectorized query generation:** * For each bucket, form a standard single‑query PIR request (hierarchical hypercube index). * Pack query vectors from multiple buckets into ciphertexts via vectorization. 6. **Request compression:** * Apply LSObvDecompress.Compress on the plaintext vector of bucket queries so that only the ℓ real queries drive the compressed encoding. 7. **Encrypt and send:** * Encrypt the compressed request and send to the server. ### 5.3 Server Processing 1. **Oblivious decompression (requests):** * Run LSObvDecompress.ObvDecompress to expand the compressed query into `1.5ℓ` logical bucket queries, without learning which are real. 2. **Bucket PIRs:** * For each bucket: * run vectorized PIR over its encoded DB subset to produce a bucket response ciphertext with (at most) one non‑zero slot. * Merge per‑bucket responses into a small set of vectorized ciphertexts (as in Vectorized BatchPIR). 3. **Oblivious compression (responses):** * Apply LSObvCompress.ObvCompress to the vector of bucket responses to shrink from `1.5ℓ` logical responses to ≈ `(1+ε)ℓ` ciphertexts. 4. **Send response:** * Return the compressed, vectorized ciphertexts + the current UBT root + MPT→UBT proof for that root. ### 5.4 Client Verification 1. **Decrypt & decompress:** * Decrypt the compressed response. * Run LSObvCompress.Decompress to recover the full set of (index, value) pairs for all requested nodes. 2. **Rebuild Merkle proofs:** * For each high‑level state value `x_j`: * recontruct its leaf value from the corresponding UBT leaf node; * collect all siblings along the path; * recompute the UBT root. 3. **Root checking:** * Verify each recomputed UBT root equals the root included in the response. * Verify the UBT root links to the on‑chain MPT root via the MPT→UBT proof. 4. **Output:** * If all checks pass, return the decrypted state values to the application (wallet, verifier, SNARK circuit, etc). The server never learns which `(stem, subindex)` pairs (hence which accounts/slots/code chunks) were actually used to assemble the Merkle proofs. --- ## 6. Efficiency Snapshot (for orientation) Concrete numbers obviously depend on parameter choices and implementation, but the underlying primitives support the following ballpark: * **Vectorized BatchPIR (no compression):** * DB: 1M entries, entry size 256 B; batch ℓ = 256. * Amortized server time ≈ 123 ms per entry. * Total comm ≈ 19× plaintext size (client+server). * **With LSObvCompress/Decompress:** * Request and response layers shrink by ≈ 30% in terms of ciphertext count for the cuckoo buckets, for both small and large entries. On top of that, the UBT’s binary structure keeps **state maintenance** cheap: * Updating one Ethereum state slot touches only `O(log N)` ≈ 24 nodes (for `N ≈ 2²⁴`) in the flattened DB and a corresponding number of hashes.