Native UTXOs on Ethereum
Special thanks to Thomas, Ignacio and Matt for feedback and review, and Vitalik for raising this topic and fostering the discussion around it.
On Ethereum, receiving a payment adds state forever. The first time an address receives ETH, it gets a permanent account leaf. The first time it holds an ERC-20, a permanent storage slot.
Bitcoin works differently. A Bitcoin payment is a one-shot object: created once, spent once, then gone, deleted from state. The chain remembers that a UTXO was spent, not an account balance.
This property is worth borrowing, and Ethereum can have it without giving up accounts. For payment workloads that do not need persistent state, native UTXOs bring the permanent state usage down by roughly 99.8%.
This post assumes Frame Transactions (EIP-8141): a frame is one self-contained, signed step inside an EIP-8141 transaction. The read-only VERIFY phase lets one frame inspect the next and refuse to approve the sponsoring, making trustless sponsorship possible. Signatures live in a list where each entry names its scheme: secp256k1 and P-256 today, post-quantum schemes later. UTXO spends reuse that list as-is.
tl;dr A single signed, value-conserving EIP-8141 frame folds UTXO inputs, UTXO outputs, account flows, and gas into one transition. Sponsorship then becomes a regular frame: a payer frame approves only after seeing an output that repays it.
One-shot payments, no permanent state
Ethereum accounts are great, but they are not well suited to one-shot payments.
Most simple payments do not need a persisting account. They need only:
- a value,
- a recipient,
- a way to spend the value once (avoid double-spending).
That is exactly Bitcoin’s UTXO model.
For Bitcoin, unspent UTXOs become state, reclaimed only when spent. We want the same one-shot value with no persistent state, so we may want to put UTXOs in history instead of state; we’ll see how below.
A UTXO’s recipient is just an Ethereum address. Spending it takes a signature from the recipient’s key, in any scheme the signature list supports: a passkey can hold a UTXO today, a post-quantum key tomorrow. Recipient code never runs, and no new scripting language is needed.
The UTXO object
Each UTXO is described by an opening and one protocol-assigned index:
opening = (source, value, recipient)
index : uint64
source: the account that created the UTXO.value: the amount in wei.recipient: the address authorized to spend.index: a global monotonic counter assigned at creation, used to prevent double-spending.
A UTXO is spent whole. Like Bitcoin, there are no partial spends: a spend consumes the entire value, and any remainder comes back as a new UTXO output or as an account transfer.
Creation is a system contract call. Anyone, including contracts, can send value to the vault UTXO_VAULT with the opening in calldata. The vault reads the amount from msg.value, assigns the next index, and emits the creation event log. The vault holds all unspent UTXO value, and only the protocol can move funds out.
Crucially, the opening itself is not stored in state. It is only emitted in the creation event log, which makes the UTXO discoverable, and committed to a per-block openings root, which makes it provable. At the end of each block, the protocol hashes every opening created in that block, leaf = hash(index, source, recipient, value), into a small binary tree and keeps the root in a ring buffer. This differs from Bitcoin, where nodes must keep the set of unspent UTXOs in state; here, nodes only need to keep the (small) state of the system contract. The creation log contains:
sourceastopics[1],recipientastopics[2],indexandvaluein the data.
This keeps the state footprint minimal while keeping UTXOs discoverable and provable.
Minimal protocol state
Keep only one fact per UTXO in state, “has it been spent?”, and prove everything else from history. The chain keeps only the following state:
| name | role | size |
|---|---|---|
next_utxo_index |
next index to assign | 1 slot, overwritten |
spent_bits[index >> 8] |
spent flags | 1 bit per UTXO |
openings_root_ring[N mod 8192] |
recent openings roots | 256 KB fixed |
batches[N / 8192] |
sealed old roots | ~10 KB/year |
UTXO_VAULT |
locked ETH | one reserved address |
The index selects a spent bit:
word = index >> 8
bit = index & 0xff
The spent set is a bitfield, not a
mapping(uint => bool). A bool mapping wastes 255 bits per slot, while a packed bitfield fits 256 flags into one word. One 32-byte word covers 256 consecutive indices, so a spent UTXO costs ~0.125 bytes of raw bitfield.
A spend proves its opening against an openings root. Recent roots are read from the ring stored in the system contract, and older roots are recovered through sealed batches, also kept in the same contract. This avoids storing one commitment per UTXO, which would recreate the state-growth problem.
The chain already commits to logs through the receipts root, but a receipt proof must carry the full receipt of the creating transaction, with every log it emitted. A batch payout that creates 500 UTXOs in one transaction would force each of the 500 spenders to ship that whole receipt as witness. In the openings tree, every UTXO is its own leaf: a proof is a few hundred bytes of plain hashing, no matter how the UTXO was created, and the verifier never touches RLP or MPT proofs. Creation still pays only for its log; the root is computed by the client at the block boundary, just like the receipts root.
Store consumption, prove existence
Step back from UTXOs: there is a general principle here. A “can this still be spent?” check answers two questions at once: “did this ever exist?” and “has it been used yet?”
Bitcoin combines both into one: presence in the UTXO set. That is elegant, but it forces the entire object into state, and the only way to reclaim that space is to delete it on spend. Ethereum accounts make it even worse: pay at creation, never reclaim. Both pay their permanent cost up front, at creation.
The UTXO design above splits the two questions instead. Existence is proven from append-only history, the opening against an openings root, and the only thing kept in state is a single “spent” bit. We won’t require everyone to keep track of the unspent UTXO set by holding all UTXOs in state. The permanent cost stops landing at creation and moves to consumption: a UTXO that is created but never spent writes no permanent state at all, and a spent one costs a bit.
Rule: keep in state only the minimal fact the next transaction’s validity depends on, usually just “has this been used?”, and push existence and contents into append-only history you prove against on demand.
The downside is that this won’t reach Bitcoin’s zero left-over. Because history cannot be un-written, every UTXO needs a “consumed” marker to prevent replay. Bitcoin’s marker is deletion itself: the UTXO simply disappears from the set; here, it is one bit that stays.
So, we are trading a large reclaimable footprint for a tiny permanent one. For partially stateful nodes, this avoids the need to keep track of all UTXOs (openings), and instead they only need the UTXO_VAULT state, growing at a negligible rate.
| Model | Existence Stored In | Consumption Stored In | State Written at Creation (Opening Size) |
|---|---|---|---|
| Bitcoin | State (UTXO set) | History (spent UTXOs) | ~50–100+ B per UTXO opening |
| Ethereum Today | State (accounts/storage) | State (account/storage updates) | ~100–150+ B account/storage entry |
| Ethereum Native UTXO | History (logs + openings roots) | State (spent bit) | 0 B |
What spending must guarantee
Creation, as shown, is the easy half: the vault (=system contract) works like the beacon deposit contract, where a call with value emits the creation log and creates the UTXO. Spending a created UTXO is the other half, and unlike creation it needs native protocol support, because a fresh recipient may hold no ETH at all.
Recipients should never have to hold ETH to spend a UTXO. The whole point is the one-shot recipient: an address that receives value once, may hold no ETH of its own, and may have no reason ever to hold any. Such an address is fresh, and funding it just to pay gas would recreate the account leaf the UTXO was meant to avoid. Single-use recipients like ERC-5564 stealth addresses are the extreme case, but the requirement holds for any fresh recipient.
A recipient must be able to spend a 1 ETH UTXO while holding zero ETH, and there are only two acceptable ways to do that:
- Self-funded spend: the UTXO pays for its own spend, out of its own value.
- Sponsored spend: someone else fronts the gas and is repaid out of the UTXO, trustlessly, so the sponsor approves only when repayment is guaranteed. Ideally, a system contract fills this role.
These two properties are the real test. A UTXO design that cannot provide them is useful only for already-funded accounts, which is not where UTXOs add the most value.
Design space: opcode or frame
There are two ways to add that native support:
1. add a spend opcode,
2. add a special-target EIP-8141 VERIFY frame.
Initially the opcode design looks simpler: expose UTXO spending directly to EVM code. The frame design looks heavier: expose a full signed transition between accounts and UTXOs.
The following sections show why the frame design is the right one: it delivers self-funded spends and trustless sponsorship. Skip the next section if you don’t care why an opcode design falls short.
Option 1: a spend opcode
The opcode design adds:
SPEND_UTXO(index, source, value, creation_block, opening_path) -> value
SPEND_UTXO resolves the openings root, verifies the opening, checks that the spent bit is unset, flips it, and credits msg.sender.
For already-funded accounts this works. A wallet creates a UTXO, a funded recipient spends it, and no EIP-8141 machinery is needed. Unfortunately the design fails where UTXOs matter most: fresh recipients.
Self-funded spends fail
A normal Ethereum transaction pays for gas before execution starts. A zero-ETH recipient cannot even begin execution, because the upfront balance check for intrinsic gas fails before SPEND_UTXO can run.
Even if execution could start, ignoring the DoS vector, the unlocked value would arrive too late. SPEND_UTXO moves value during execution, but gas is already prepaid. Mid-execution value cannot cover a pre-execution charge.
So a UTXO cannot fund its own spend through an opcode, and the underlying reason is that the protocol cannot see what the transaction is about to do before running it.
We would probably need a relayer:
SPEND_UTXOcredits and authorizesmsg.sender, so the spend must run with the recipient as caller. For a third party to trigger that, the recipient needs code of its own: a smart wallet or EIP-7702 delegation. That writes a permanent account leaf for the recipient, which defeats the point for one-shot payments.
Trustless sponsorship fails
A sponsor, potentially a contract, needs to know that it will be repaid before it pays.
In EIP-8141 terms, the payer should inspect the next frame, see a repayment to itself, and approve only if repayment is guaranteed. A normal transaction has no such structure. It has one gas payer fixed before execution, no protocol-level payer step, and no structured repayment field.
For example, contracts can introspect other frames, but they cannot simulate EVM code to find out whether a spend would repay them.
Sponsorship therefore has to move into relayers, meta-transactions, ERC-4337, or bespoke contract code. There may be a way to make this work, but the repayment logic lives outside the UTXO object, and it is not the clean primitive we want.
The opcode inside a frame still does not fix it
Putting SPEND_UTXO inside an EIP-8141 transaction does not solve the problem.
The payer is approved in a VERIFY frame, which runs as STATICCALL. But SPEND_UTXO changes state: it flips the spent bit and moves vault balance. It cannot run in VERIFY. It must run later, after payment is already committed, so the unlocked value still arrives too late.
Sponsorship still fails, too. The sponsor can parse the data passed to a later frame but cannot determine whether that frame actually repays it by calling the opcode.
The issue is structural: an opcode is just EVM code. It runs after the payer is fixed, and it gives other frames no clean object to inspect.
Option 2: a value-conserving frame
A spend is a signed, value-conserving transition: UTXO inputs in, UTXO outputs and account payments out, gas included.
EIP-8141 can expose that transition directly as a canonical VERIFY-frame target with protocol-defined settlement semantics. The target is UTXO_VAULT itself, but its code never runs here. The protocol recognizes the address, like a canonical paymaster or expiry verifier, and applies the semantics below. Vault code runs only for deposits: the outputs of a spend, change included, are created by the protocol directly.
The frame looks as follows:
actors: [address] // each backed by a scheme-tagged signature
// from the tx signature list over the frame digest
inputs: [
{ index, creation_block, opening_path }
]
utxo_outs: [
{ recipient, value }
]
account_outs: [
{ recipient, value }
]
change_index: uint
payer: address
max_fee_per_gas
max_priority_fee_per_gas
max_gas_limit
Each actor signs the whole frame except the opening_path witnesses. Concretely, the signatures cover every field but the witnesses. Refreshing a witness therefore leaves them valid: the submitter re-wraps the outer transaction around the new witness without ever touching them. The signatures themselves are ordinary entries in the transaction’s signature list: secp256k1 and P-256 work today, and a post-quantum scheme is just one more tag, with the frame format unchanged. The signatures bind the inputs, utxo outputs, account flows, fee caps, and chain ID. No submitter can redirect value or alter the transition, but anyone can refresh a witness, for example append the batch path once the creation leaves the ring. A substituted witness either proves the same signed creation or fails verification, since each input is pinned by its globally unique index. Using utxo_outs, new UTXOs can be created, and using account_outs, one can spend directly to an account balance.
An input carries only index and creation_block; source, value, and recipient are read from the proven opening rather than signed, since the unique index already pins them. Verification requires each input’s proven recipient to be one of the actors, so one frame can consolidate UTXOs held by several addresses: ten stealth payments merge in a single spend, for a single fee. With one actor, each output is created with source = actor; with several, outputs carry source = UTXO_VAULT.
Every frame consumes at least one input, so the spent bits are the replay protection: once set, the frame cannot run again.
The protocol enforces the following conservation rule:
sum(inputs.value)
>=
sum(utxo_outs.value) + sum(account_outs.value) + max_cost
max_cost is the transaction’s maximum fee (TXPARAM(0x06)): all gas at max_fee_per_gas, including the intrinsic, per-frame, calldata, and signature costs. Reserving the maximum guarantees the inputs cover the fee. change_index marks one utxo_out (or account output) as change. Its signed value is zero and it is excluded from the sums above. A transaction carries at most one UTXO frame, since the fee and max_cost are transaction-scoped. In the sponsored case (payer != 0) the max_cost term is replaced by a signed repayment output to the payer; see Trustless sponsorship below.
Gas lives inside the conservation rule, not outside the transition, so the UTXO pays its own transaction fee.
Self-funded spends
The consumed UTXO inputs cover:
- new UTXO outputs,
- account outputs,
- gas.
No actor needs ETH in its account. The vault pays from the UTXO value being consumed. The gas includes a priority fee, also paid from the UTXO, so the proposer that includes the spend is paid even though the actor holds no ETH.
This is what the opcode design cannot express. With an opcode, gas is charged before the spend. With the frame, spending and fee payment are one verified transition. A one-shot recipient can receive a UTXO, spend it later, and never hold an account balance.
Trustless sponsorship
Sponsorship can be achieved with frames too.
In a self-funded spend the UTXO pays the protocol gas itself, which is the max_cost term of its conservation rule. When a payer sponsors the transaction, the payer pays the protocol gas instead. The UTXO then drops the max_cost term and carries an ordinary signed repayment output to the payer, sized to at least the payer’s gas cost. The repayment can be a utxo_out or an account_out; a sponsor is a funded account anyway, so an account credit spares it another spend. The actor sets aside the same value either way; the payer pays the actual fee and is repaid in a later frame, so the spread between the reserved amount and the actual fee is the payer’s compensation for fronting gas. The signed payer field commits to the mode: a submitter cannot strip the sponsor frame and re-run the frame as self-funded, because a non-zero payer must approve the payment.
The payer frame introspects the next frame and checks the structured outputs:
exists output in utxo_outs + account_outs where
output.recipient == payer
output.value >= required_payment
The payer approves only if such an output exists, and rejects otherwise. If the VERIFY frame fails, the whole transaction fails and the payer is never charged. This removes the entire trust requirement, and the sponsor could even be a contract.
Concrete frame semantics
Here is the same transition stated in more detail, phase by phase: validate, approve, settle.
VERIFY phase
VERIFY is read-only. For each UTXO frame:
- For each actor, check its signature over the frame in the transaction’s signature list.
- For each input, of which there must be at least one:
- resolve the openings root for
creation_block, - verify the opening path against the root and locate the leaf carrying
index, - read
source,value, andrecipientfrom that opening, and requirerecipientto be one of the actors, - require the spent bit at that
indexto be unset.
- resolve the openings root for
- For each output:
- require
recipient != address(0).
- require
- Check the conservation rule, with the change entry selected by
change_indexexcluded. - Check fee caps:
- the envelope’s
max_fee_per_gasandmax_priority_fee_per_gasare at most the signed caps, tx.gas_limit <= max_gas_limit.
- the envelope’s
The proven opening is one leaf of the creation block’s tree:
leaf = hash(index, source, recipient, value)
The witness supplies the opening’s fields and the path to the root.
A spend always proves opening -> openings_root; the verifier reads the root from the ring. After some time, once it is sealed into a batch, the spender appends one more path, openings_root -> batch_root, and the verifier checks it against the batch root in state. The base proof never changes. Leaving the ring only adds the batch path, which anyone can rebuild from public chain data. A spend therefore never depends on a mutable ring slot and can be kept valid forever.
Approval
When the frame is approved, the protocol applies one atomic step, recorded outside the frame revert journal in the same way EIP-8250 records a consumed nonce, so a later frame failure cannot undo it:
- For each input, check-and-set its spent bit at the verified
index. If it is already set, the frame fails. - If the frame’s
payeris zero, setpayer = UTXO_VAULT. Otherwise the named payer must approve the payment, or the transaction is invalid.
Setting the spent bit here gives the spend-once guarantee and removes double-counting: a duplicate input, in this frame or a later one, sees the set bit and fails. The spend is therefore final once approved, even if a later frame reverts.
Settlement
Settlement runs for every approved frame and cannot fail, because VERIFY already proved it solvent:
- Debit the vault by each input’s value.
- Create each output: increment
next_utxo_index, credit the vault, emitUtxoCreated. The change output receives the left-over. - Apply
account_outs. - Pay the actual fee: burn the basefee and send the priority fee to the proposer.
Validation reads only protocol state, the openings roots, and the vault’s own spent bits, never arbitrary account state, so a UTXO frame is cheap to validate and safe to treat like any other transaction in the public mempool. This is also why spending is a signature check and recipient code never runs: if code decided whether a spend is valid, any storage write could invalidate pending spends in the mempool.
The second rule: declare transitions, don’t execute
The frame design works because it declares the whole transition as inspectable data. The opcode fails because it is code that runs after the gas payer is already fixed, and it hands other parties no object to inspect before they commit value. The frame succeeds because it expresses the whole transition as signed, value-conserving data that the protocol, and a prospective sponsor, can validate before any value moves.
This is why folding gas into the conservation rule matters, and why sponsorship becomes trustless. The decision points, “does this transition conserve value?” and “does this output repay me?”, are answered by reading the frame, not by running anything. Making the transition statically inferable is what turns both success properties from awkward workarounds into a clean primitive.
That completes the core design. The rest is practical detail: how recipients discover their UTXOs, how tokens fit, where the spent bits live in the state tree, and what it all costs.
Discovery
Recipients discover their UTXOs by scanning logs under UTXO_VAULT for their address in topics[2]. Both the emitter and the signature topic (topics[0] = UtxoCreated_sig) must be checked, or a future log under the vault could spoof a match.
Stealth recipients are the exception: an ERC-5564 stealth address is derived fresh per payment, so the recipient cannot pre-filter by
topics[2]and scans by view tag instead.
This is also where the design pays off with respect to privacy. Because recipient is just an address, it can be a stealth address, and because the spend is self-funded, that fresh address never has to hold ETH or appear as tx.sender. The result is recipient unlinkability across payments. The sender is still exposed through topics[1], so sender privacy would need a separate mechanism such as mixing.
Tokens
ERC-20s can work too, with a small extension: the opening would contain a token_address, with address(0) for ETH. The global index, the spent bitfield, the opening proofs, and discovery are unchanged, since an index is an index regardless of asset: tokens share the same index space and the same ~0.3-byte spent cost. An input’s token_address, like its source and value, is read from the proven opening, not signed.
Creation deposits real tokens. create(token, value, recipient) pulls value with transferFrom and records the amount actually received, so the vault always holds exactly what it owes. The vault becomes a per-token escrow. Spending is the same frame, but conservation is enforced per token: for each token, the inputs must cover that token’s utxo_outs and account_outs, and the max_cost gas term applies only to the ETH bucket. Each token keeps its own change output. A single spend may carry several tokens at once but never swaps one for another, since the vault is escrow, not an exchange.
Crucially, the spend frame makes no external token calls. Settlement only updates the vault’s internal accounting: it sets spent bits, emits the UtxoCreated logs, and for a token account_out credits an internal balance that the recipient later pulls with a separate withdraw. A pure UTXO-to-UTXO token flow therefore touches the token contract exactly once, at the original deposit, and never again. This keeps settlement unable to fail and keeps VERIFY reading only protocol state, so token spends inherit the same mempool-safety as ETH spends (see Settlement), and a misbehaving token can only affect its own holders. Tokens also remain free to block accounts; a blocked account cannot deposit into the vault.
Gas is the one place tokens differ: it is paid in ETH, so only ETH UTXOs can self-fund their spend. A token-only spend must therefore always be sponsored: a payer fronts the ETH and takes its repayment in the token.
Where the spent bits live in the state tree
In EIP-7864 the first byte of every key is a storage type that labels the kind of data it holds: account headers, contract code, storage, and some reserved for future categories, like the spent bitfield. So the spent set gets its own type, and we key it by index instead of by hashing.
The index is assigned by the protocol in sequence, so it maps directly into the tree key, high bits first, and consecutive UTXOs are adjacent:
index : uint64
index & 0xFF -> which bit inside the leaf (a leaf is 256 bits)
(index >> 8) & 0xFF -> which leaf in the stem (a stem holds 256 leaves)
index >> 16 -> the stem itself, placed right after the type byte
One 32-byte leaf is 256 flags and one stem is 256 leaves, so a single stem covers 65,536 consecutive UTXOs, and the tree’s order is the index’s order. This is the same spent_bits[index >> 8] word as before; the index determines which stem, which leaf, and which bit.
The rest of the protocol state is small: next_utxo_index is a single counter and lives in the vault’s account header beside its balance. The openings-root ring and the sealed batches are bounded and can stay in the vault’s storage.
Placement rule: a system-assigned, ever-growing, write-once set belongs under its own type byte, keyed by index, not by its hash. Indices as keys keep it ordered and cheap to prove in batches. This also works well with VOPS, since it cleanly separates storage from “special” state such as nullifiers or spent bits.
Costs
Permanent state
| model | state per entry | at 1B entries | after use |
|---|---|---|---|
| fresh account / holder slot | ~100–150 B | ~100–150 GB | leaf remains |
| native UTXO | ~0.3 B | ~300 MB | spent bit remains |
For payment workloads that do not need persistent state, native UTXOs are roughly two orders of magnitude cheaper in permanent state than the account model.
Example: Alice pays Bob with a UTXO
Alice has 10 ETH and wants to give Bob a 1 ETH UTXO. Bob is 0xB0.
Creation
Alice calls the vault:
to: UTXO_VAULT
value: 1 ETH
data: create(recipient = 0xB0)
After execution:
UtxoCreated(
addr = UTXO_VAULT,
topics = [UtxoCreated_sig, 0xA1, 0xB0],
data = (42, 1 ETH)
)
State changes:
next_utxo_index: 42 -> 43
Alice: 10 ETH -> 9 ETH - actual fee
vault: 0 -> 1 ETH
At the end of the block:
openings_root_ring[N mod 8192] = openings_root_N
# commits leaf = hash(42, 0xA1, 0xB0, 1 ETH)
Bob’s wallet filters for logs under UTXO_VAULT with topics[2] = 0xB0 and stores the opening. No out-of-band message from Alice is needed.
Spend
Three blocks later, Bob spends the UTXO. He sends 0.4 ETH to Charlie and 0.5 ETH to Dave, and takes the remaining value back as a change UTXO, after fees:
fee = gas_used * effective_gas_price
Exactly as in Bitcoin, the change is a new output, not a left-over account balance. Its value is set at settlement.
actors: [0xB0]
inputs: [
{
index: 42,
creation_block: N,
opening_path: ...
}
]
utxo_outs: [
{ recipient: 0xC4, value: 0.4 ETH },
{ recipient: 0xD5, value: 0.5 ETH },
{ recipient: 0xB0, value: 0 }
]
account_outs: []
change_index: 2
payer: 0
Any node can submit the frame. Bob’s account balance is never touched, because the spent UTXO funds the whole transition.
After settlement:
spent bit 42: 0 -> 1
next_utxo_index: 43 -> 46
vault: 1 ETH -> 1 ETH - fee
Charlie, Dave, and Bob’s change are now three independent UTXOs backed by the vault. No account state is touched.
account_outs is optional. It is the escape hatch back into the account model. A pure UTXO-to-UTXO spend leaves no account state behind.

