Thanks to Toni Wahrstätter for suggesting this line of work, and to Zsolt Felföldi for his work on the Trustless log and transaction index.
TL;DR: This writeup explores how a wallet can discover UTXOs without trusting an RPC provider to return a complete or accurate log result. The proposed construction combines EIP-8304 recipient-range proofs with a per-block UTXO Proof Table (UPT). EIP-8304 proves where a recipient was mentioned; UPT supplies selected opening records and Merkle material that can be checked against the native per-block openings root.
The prototype demonstrates that the two proof systems can be composed without adding an application-specific entry type to EIP-8304. Receipt-log discovery remained the simplest and fastest path for an uncached local scan. After UPT records and proofs were verified and cached, the prototype reused substantially less remote data. That warm result reflects the prototype’s cache policy and is not a general claim that UPT is inherently faster than an equivalently cached receipt wallet.
Status and scope
EIP-8304 is a draft proposal. The native UTXO design referenced here is an Ethereum Research proposal rather than a finalized protocol specification. The integration described in this writeup is experimental and is not part of either proposal.
The implemented profile includes:
- standard EIP-8304 entry types 0 through 6;
- an application-specific four-topic
UtxoCreatedevent; - per-block UPT objects using format version 2;
- selected-record opening multiproofs;
- block-hash-keyed node persistence;
- batched UPT retrieval;
- recent openings-root checks;
- in-process wallet caches.
It does not yet include:
- independently authenticated execution headers;
- wallet-side verification of account and storage proofs against those headers;
- an archive and batch-path service for old openings roots;
- a standardized UPT RPC or wire format;
- a UPT availability or retention network;
- receipt-free construction of final per-input spend witnesses.
The prototypes use Ethrex-specific RPCs such as ethrex_queryEip8304Table and ethrex_getUtxoProofs, plus a selected-log RPC in the earlier full-log experiment. These are not standard Ethereum JSON-RPC methods and are not defined by EIP-8304.
The discovery problem
Suppose Alice creates a 1 ETH UTXO for Bob:
UtxoCreated(
source = Alice,
recipient = Bob,
index = 2191,
value = 1 ETH
)
Bob’s wallet needs to learn at least:
- the global UTXO index;
- the source and recipient;
- the value;
- the creation block and event position;
- whether the output is currently unspent;
- enough authenticated evidence to reject forged or omitted results.
In this writeup, an EIP-8304 event position is:
(block_number, transaction_index, transaction_relative_log_index)
Standard JSON-RPC log objects use a block-relative logIndex, so an implementation comparing the two representations must normalize them explicitly.
The native UTXO state does not retain every opening in full. It retains a spent bit for each global index and a ring of recent per-block openings roots. Older per-block roots may later be sealed into batch roots.
An openings root authenticates a record only after the record and a valid Merkle path have been obtained. It does not reveal which opening belongs to Bob, provide the opening’s value, or supply missing sibling hashes.
Discovery therefore contains four different questions:
| Question | Required evidence |
|---|---|
| Where was Bob mentioned? | A complete EIP-8304 recipient-range proof |
| What opening fields were committed? | An opening record proven against the block’s native openings root |
| What Merkle material reconstructs that root? | A selected-record proof or shared per-block multiproof |
| Is the output spendable now? | A current spent-bit value proven against authenticated canonical state |
No single structure in the current design answers all four questions.
EIP-8304 as the discovery index
EIP-8304 creates ordered per-block and aggregated index tables. The standard entry types are:
| Type | Indexed content |
|---|---|
| 0 | Block hash |
| 1 | Transaction hash |
| 2 | Emitting log address |
| 3 | topics[0], usually the event signature |
| 4 | topics[1] |
| 5 | topics[2] |
| 6 | topics[3], when present |
Entries are ordered lexicographically by their binary encoding. Log entries also carry their block number, transaction index, and transaction-relative log index.
This ordering lets a server binary-search for every type 5 entry containing Bob’s padded address. A proof containing the matching interval, the adjacent non-matching boundaries, the relevant leaf indices, and the table length can establish that no matching entry was omitted from that table.
The recipient range alone is not enough. Any contract can place Bob in topics[2]. At every candidate position, the wallet must also authenticate at least:
- type 2: the native UTXO vault address;
- type 3: the
UtxoCreatedevent signature.
Other topic entries can be requested when the event schema exposes more opening fields.
Three methods explored by the prototype
These are three evaluated constructions, not an exhaustive list of every possible discovery design.
Method 1 — standard eth_getLogs
The wallet requests logs over a block range using:
- the UTXO vault address;
- the
UtxoCreatedsignature; - Bob’s recipient topic.
The returned log contains all indexed topics, event data, transaction metadata, and block metadata needed to decode the opening.
This method is operationally attractive:
- it uses a standard RPC;
- it requires no new availability object;
- it returns the event data directly;
- a well-indexed provider can answer common queries efficiently.
Its core limitation is trust. eth_getLogs does not prove that the provider returned the complete canonical result set. Receipt proofs could authenticate individual logs, but they do not by themselves prove that every Bob event in a range was returned.
In the Ethrex prototype used for the local experiment, eth_getLogs checks block-header blooms and loads block bodies and receipts for candidate blocks. It should therefore be described as the Ethrex receipt-scan baseline, not as a benchmark of every production provider’s log-index architecture.
Method 2 — EIP-8304 with a custom full-log entry
The first authenticated prototype added a non-standard type 7 entry committing to a complete raw log. Conceptually:
SHA256(
domain ||
log_address ||
encoded_topic_count || encoded_topics ||
encoded_data_length || data
)
An interoperable specification would need to fix the exact domain bytes, integer widths, byte order, and list encoding. The implemented prototype used the domain string EIP8304_LOG_V1.
The wallet first proves the complete Bob recipient range and the relevant entries at each candidate position. It then retrieves only the selected raw logs and checks each one against its type 7 commitment.
This construction provides authenticated log payloads and range completeness, but it has important costs:
- it changes the EIP-8304 entry set;
- every indexed log adds another table entry;
- selected raw-log bytes must remain available;
- repeated scans still retrieve the selected logs unless they are cached separately;
- the custom entry is application-independent in payload but non-standard in protocol.
The prototype is useful as a comparison point, but it is not proposed here as an extension to EIP-8304.
Method 3 — EIP-8304 with a UTXO Proof Table
The UPT construction keeps standard EIP-8304 types 0 through 6 unchanged.
EIP-8304 is used only for search and range completeness. Opening data is authenticated separately against the native UTXO openings root.
This separation reflects the fact that the structures have different orderings and purposes:
| Structure | Ordering | Purpose | Authenticating commitment |
|---|---|---|---|
| EIP-8304 table | Encoded entry content and event position | Find every event mentioning Bob | EIP-8304 table root |
| Opening tree / UPT | Global UTXO index within the block | Recover and prove opening fields | Native per-block openings root |
The UPT is not consensus state. It is a content and serving format for data already committed by the native openings root.
Event-schema distinction
The prototype benchmark used this application-specific event:
event UtxoCreated(
address indexed source,
address indexed recipient,
uint64 indexed index,
uint256 value
);
Its log layout is:
address = UTXO_VAULT
topics[0] = keccak256("UtxoCreated(address,address,uint64,uint256)")
topics[1] = left_pad_32(source)
topics[2] = left_pad_32(recipient)
topics[3] = left_pad_32(index)
data = uint256_be(value)
The native UTXO research proposal currently describes:
topics = [UtxoCreated_signature, source, recipient]
data = (index, value)
The difference affects more than event decoding:
| Property | Current research proposal | Prototype schema |
|---|---|---|
source searchable through EIP-8304 |
Yes, type 4 | Yes, type 4 |
recipient searchable through EIP-8304 |
Yes, type 5 | Yes, type 5 |
index searchable through EIP-8304 |
No | Yes, type 6 |
| Additional EIP table entry per event | No | Yes |
| Exact index-to-event join from topics | No | Yes |
In the prototype schema, the wallet cross-checks UPT index, source, and recipient against type 6, type 4, and type 5 entries at the same event position.
With the current proposal’s schema, the UPT proof still authenticates the opening fields and the EIP proof still authenticates a Bob event emitted by the vault. However, the openings leaf does not commit to the transaction-relative event position. If a block contains multiple outputs with the same source and recipient, a selective service can permute their event-position associations without breaking either root.
A production design using the current event schema must decide whether exact event association is required and, if so, how to bind it. Options include a receipt/log proof, an explicit position commitment in the opening design, a canonical derivation rule with sufficient data, or an event-schema change. The prototype’s type 6 binding must not be presented as a property of the current native proposal.
The UTXO Proof Table
Opening tree
The Ethrex prototype defines the opening leaf as:
opening_leaf = keccak256(
uint64_be(index) ||
source ||
recipient ||
uint256_be(value)
)
The preimage is 80 bytes. Leaves are ordered by global UTXO index, padded with zero hashes to the next power of two, and folded with ordered pairs:
parent = keccak256(left || right)
These byte-level and padding rules are prototype rules. They must become part of the native UTXO specification, or be replaced by whatever canonical opening-tree definition that specification adopts.
Record format
Each prototype record is 88 bytes before transport encoding:
UtxoProofRecord
├── index: uint64 // 8 bytes
├── source: address // 20 bytes
├── recipient: address // 20 bytes
├── value: uint256 // 32 bytes
├── transaction_index: uint32 // 4 bytes
└── transaction_relative_log_index: uint32 // 4 bytes
--------
88 bytes
The block-level object contains:
UtxoProofTable (prototype format version 2)
├── format_version
├── chain_id
├── vault
├── block_number
├── block_hash
├── openings_root
├── records[] // ascending global UTXO index
└── internal_nodes[] // cached opening-tree internal hashes
For U > 0 records, let P = next_power_of_two(U). Records provide the leaf preimages, while the prototype stores P - 1 internal hashes.
The internal-node array is a node-side proof-generation optimization, not cryptographically necessary data. A node retaining all records can reconstruct every internal node. A complete evaluation should measure the storage saved by reconstruction against the CPU and latency saved by retaining the internal-node index.
Content identifier
The prototype computes:
table_hash = SHA256("UPT_BLOCK_V2\0" || canonical_ssz_bytes)
The table hash identifies one complete serialized UPT object. It is not a consensus root and does not authenticate a response merely because the server returns it alongside that response.
Selected records are authenticated by recomputing their opening leaves and folding the supplied multiproof to an independently authenticated native openings root. The table hash could still be useful for content addressing, manifests, mirrors, or an availability network.
Construction and persistence
After a block executes, the prototype UPT builder:
- parses surviving
UtxoCreatedlogs from the block’s receipts; - creates one record per decoded opening;
- sorts records by global UTXO index;
- rejects non-consecutive indices or duplicate event positions;
- computes the canonical opening leaves and root;
- builds the cached internal-node representation;
- stores the object in RocksDB under the exact block hash.
Block-hash keys isolate competing forks. Canonical RPC handling resolves the current canonical hash before loading a table, although independently verifying that hash remains a wallet responsibility.
If a recent UPT is absent, the prototype can reconstruct it from retained receipts. That is an implementation convenience, not a long-term availability solution once history is pruned.
Recipient-first query protocol
The wallet does not request every vault-address or event-signature entry in a table. Those ranges scale with total UTXO traffic. It starts with one complete recipient range:
ethrex_queryEip8304Table(
first_block,
table_size,
[{ typeId: 5, content: left_pad_32(Bob) }],
[2, 3, 4, 6]
)
The [2, 3, 4, 6] candidate fields apply to the prototype’s four-topic event. A query for the current native proposal would not request type 6.
For each table, the service returns:
- every matching type 5 recipient entry;
- the immediate lower and upper range boundaries when they exist;
- the requested candidate entry types at each matched position;
- transaction entries needed to associate transaction hashes;
- the table length and leaf positions;
- one shared table multiproof.
The wallet verifies the table proof, the complete recipient interval, and the candidate classification. It keeps only positions where the emitting address and signature identify the native UTXO event.
Batched selected-record retrieval
After EIP-8304 verification, the prototype sends all uncached positions in one request, subject to an implementation cap:
ethrex_getUtxoProofs([
{
blockNumber,
transactionIndex,
logIndex // transaction-relative
},
...
])
The server groups positions by block and returns:
blocks[]
├── formatVersion, chainId, vault
├── blockNumber, blockHash
├── openingsRoot, rootStorageSlot
├── tableHash, total recordCount
├── selected records[]
└── one shared proofNodes[] set
For each returned block, the wallet must verify:
- expected format, chain ID, vault, and block grouping;
- every requested event position produced exactly one record;
- no unexpected or duplicate record was returned;
- all topic fields available under the chosen event schema match the EIP proof;
- the opening leaf recomputes from index, source, recipient, and value;
- every required multiproof node is present;
- no unused proof node is accepted;
- the multiproof folds to the authenticated openings root.
The current prototype obtains opening-root storage values from its execution RPC. A production light wallet must additionally verify the vault account and storage proof against a trusted execution header.
Root of trust
The complete production verification chain is:
Without the authenticated header and state-proof steps, the wallet proves only that the provider returned mutually consistent roots and data. It does not prove agreement with the canonical Ethereum chain.
This distinction applies equally to EIP-8304 table roots, openings roots, block hashes used for caching, and spent bits.
Wallet caching and reorganization handling
The prototype caches:
- verified EIP-8304 query results by range and table end-block hash;
- table-root lookups by block hash, storage slot, and root;
- verified UPT records by exact block hash and event position.
Before reusing a table query, it re-reads the table’s end-block hash. If the hash changed, it clears all discovery caches. Because a descendant block commits to its parent chain, changing any block inside the covered range also changes the later canonical hash.
This check is useful for local cache invalidation but is not itself trustless when both hash values come from the same RPC. A production wallet should compare cache keys with authenticated canonical headers and should define a clear policy for finalized, safe, and unfinalized history.
Caching is not unique to UPT. A receipt-based wallet can also retain decoded openings and revalidate only a recent suffix. Any future performance comparison should give both approaches equivalent persistence and reorganization policies.
End-to-end lifecycle
1. Creation
- Alice or a protocol settlement creates an output for Bob.
- The vault assigns the next global UTXO index.
- A surviving
UtxoCreatedevent records the opening. - If execution reverts, neither the log nor opening survives.
The opening’s source is the entity recorded by the UTXO transition, not necessarily the outer transaction sender. This matters for sponsored transactions and protocol settlement.
2. Block commitments
- EIP-8304 derives standard address and topic entries from canonical receipts.
- Entries are sorted into the relevant EIP-8304 tables.
- Native UTXO processing orders openings by global index and commits the per-block openings root.
- The non-consensus UPT object is built and stored under the block hash.
EIP-8304 and the openings tree are derived from the same execution history but authenticate different claims.
3. Discovery
- Bob’s wallet decomposes its requested history into available aligned EIP-8304 tables.
- It proves every complete Bob recipient range.
- It classifies candidate positions by vault and event signature.
- It retrieves selected UPT records for those positions.
- It obtains and authenticates the relevant openings roots.
- It verifies the opening multiproofs and schema-dependent joins.
- It caches verified records under authenticated block identities.
4. Spendability and spending
The UPT does not store spent status because spent status changes over time. The wallet obtains the current spent bit for each discovered global index and verifies it against authenticated current state.
A spend input ultimately needs data equivalent to:
index
creation_block
source
recipient
value
opening_position
opening_siblings
batch_siblings
For a recent UTXO, the opening proof terminates at a root still present in the recent ring. For an older UTXO, the witness also needs the path from that per-block root to its sealed batch root.
The current demo transaction forger does not yet turn a returned shared UPT multiproof into each input’s canonical spend witness. It still reads the creation block’s logs and rebuilds the opening tree. The prototype therefore demonstrates discovery proof composition, not a complete receipt-free spending pipeline.
Worked opening proof
Assume one block creates four UTXOs in ascending global-index order:
position 0: #40 Alice -> Dave 2 ETH
position 1: #41 Carol -> Erin 0.5 ETH
position 2: #42 Alice -> Bob 1 ETH
position 3: #43 Frank -> George 3 ETH
Under the prototype schema, the EIP-8304 entries at Bob’s event position include:
type 2: UTXO_VAULT
type 3: UTXO_CREATED_TOPIC
type 4: left_pad_32(Alice)
type 5: left_pad_32(Bob)
type 6: left_pad_32(42)
The selected UPT record is:
{
index: 42,
source: Alice,
recipient: Bob,
value: 1 ETH,
transaction_index: 7,
transaction_relative_log_index: 2
}
Let the opening leaves be L40, L41, L42, and L43:
openings_root R
/ \
H01 = H(L40,L41) H23 = H(L42,L43)
/ \ / \
L40 L41 L42 L43
The selected proof for L42 needs sibling material equivalent to L43 and H01:
L42 = keccak256(uint64_be(42) || Alice || Bob || uint256_be(1 ETH))
H23 = keccak256(L42 || L43)
R = keccak256(H01 || H23)
assert R == authenticated_openings_root
The prototype accepts the output only when:
- the EIP-8304 proof establishes a complete Bob range and the expected vault event;
- the prototype’s type 6 entry agrees with UTXO index 42;
- the selected opening folds to the authenticated openings root;
- the current authenticated spent bit is clear.
The type 6 check is specific to the prototype schema.
Security properties
Assuming the commitment roots and canonical headers are independently authenticated, the design can address:
| Provider behavior | Wallet check |
|---|---|
| Omit one Bob event from a table | Verify the complete sorted recipient range and its boundaries |
| Return an event from another contract | Require the vault address at the same proven position |
| Return a different event type | Require the creation signature at the same proven position |
| Change an opening field | Recompute the leaf and verify it against the openings root |
| Omit a requested UPT record | Require exactly one record for every selected event position |
| Add unrelated proof material | Reject unused or duplicate multiproof nodes |
| Reuse data from another fork | Key caches by authenticated canonical block identity |
| Return stale spendability | Verify spent state at a recent authenticated header before spending |
| Withhold UPT or table bytes | Not prevented by commitments; use another source or retained wallet data |
Commitments provide integrity and, for the sorted EIP range, query completeness. They do not provide data availability.
Prototype performance observations
The local devnet experiments are best interpreted as implementation observations rather than general performance results.
The two authenticated methods were measured in separate devnet runs, so each should be compared with the receipt baseline from the same row rather than directly with the other authenticated method. Cold values are first-scan measurements; warm values are medians from repeated scans.
| Experiment | Window | Cold latency, receipt / proof path | Cold response, receipt / proof path | Warm median, receipt / proof path | Warm response, receipt / proof path |
|---|---|---|---|---|---|
| Custom full-log type 7 | 100 blocks | 12.592 / 268.189 ms | 134,642 / 1,947,887 B | 8.790 / 10.711 ms | 134,642 / 129,770 B |
| Custom full-log type 7 | 150 blocks | 11.828 / 426.530 ms | 226,906 / 3,033,305 B | 9.903 / 12.630 ms | 226,906 / 218,698 B |
| EIP-8304 + UPT | 100 blocks | 15.563 / 307.525 ms | 188,625 / 658,745 B | 15.727 / 5.889 ms | 188,625 / 7,500 B |
| EIP-8304 + UPT | 150 blocks | 14.387 / 386.561 ms | 296,973 / 1,027,430 B | 15.757 / 4.777 ms | 296,973 / 11,323 B |
Uncached discovery
The standard receipt-log path was faster for the first local scan. It performed less proof construction and verification and transferred less data than the UPT path.
Both authenticated methods paid additional costs for:
- decomposing the block window into EIP-8304 tables;
- constructing complete range evidence;
- transferring table entries and multiproofs;
- verifying table proofs in the wallet;
- retrieving and authenticating selected payloads or UPT records.
The UPT opening proofs themselves were relatively compact. Much of the cold response came from the EIP-8304 candidate entries and JSON representation rather than from the selected opening multiproof.
Repeated discovery
The custom full-log method continued to retrieve selected raw logs and therefore retained payload cost proportional to the number of matches.
The UPT prototype cached verified table results and selected UTXO records. Repeating the same historical scan therefore required mainly block-identity checks and reused the opening data locally. Under that cache policy, the UPT path completed faster and retrieved substantially fewer response bytes than a fresh eth_getLogs call.
This is evidence that authenticated UPT records are cacheable and reusable. It is not a cache-equivalent comparison: the experiment did not give the receipt path a wallet-side decoded-log cache. A receipt wallet that already retained the same historical outputs would also avoid downloading them again.
What the experiment supports
The prototype supports these limited conclusions:
- EIP-8304 range proofs and native opening proofs can be composed in one wallet flow.
- Selected UPT records can replace raw-log retrieval for opening fields.
- Shared per-block multiproofs avoid sending one independent opening path per UTXO.
- Proof construction and verification dominate uncached authenticated discovery in the current implementation.
- Verified opening records can be reused safely when caches are tied to authenticated canonical block identities.
It does not establish that UPT is generally faster than eth_getLogs, that one authenticated method is faster than the other across implementations, or that the measured behavior will carry over to remote mainnet infrastructure.
Limitations and open work
- Draft dependencies: both EIP-8304 and the native UTXO design may change.
- Prototype event schema: the benchmark relies on an indexed UTXO index not present in the current native proposal.
- No trusted header chain: roots and canonical hashes were supplied by the same execution endpoint.
- Recent roots only: old openings and batch paths are not served.
- No UPT availability model: commitments do not guarantee retention or serving.
- No cache-equivalent baseline: receipt results were not cached like UPT records.
- Local environment: network latency, remote provider limits, and heterogeneous clients were not modeled.
- Small measurement set: the experiment is insufficient for stable tail-latency claims.
- Different authenticated runs: the custom type 7 and UPT prototypes were measured on different devnet histories.
- Dense workload: other activity densities and table alignments may produce different costs.
- No spent-state benchmark: the measured scan discovered openings but did not compute an authenticated current balance.
- No final spend-witness integration: the transaction forger still reconstructs opening paths from logs.
- No node cost model: UPT storage, build time, synchronization, pruning, and denial-of-service limits need evaluation.
- No standardized encoding: the UPT object, proof-node format, and RPC methods remain implementation-specific.
Conclusion
EIP-8304 and the native openings root solve complementary parts of UTXO discovery.
EIP-8304 can prove that every event mentioning Bob in a covered table was returned. The openings root can prove the fields of a selected UTXO. A UTXO Proof Table connects those commitments by making selected opening records and shared Merkle material available without requiring the wallet to download complete receipts or raw logs.
The construction is promising as an authenticated discovery method, but its value is primarily in proof composition and selective data retrieval—not in an established universal speed advantage. A production design still needs an exact schema binding, authenticated headers and state proofs, old-root witness serving, availability incentives, standardized encodings, and a complete path from discovery proof to spend witness.




