Native UTXOs on Ethereum

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:

  • source as topics[1],
  • recipient as topics[2],
  • index and value in 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:

  1. Self-funded spend: the UTXO pays for its own spend, out of its own value.
  2. 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_UTXO credits and authorizes msg.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:

  1. For each actor, check its signature over the frame in the transaction’s signature list.
  2. 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, and recipient from that opening, and require recipient to be one of the actors,
    • require the spent bit at that index to be unset.
  3. For each output:
    • require recipient != address(0).
  4. Check the conservation rule, with the change entry selected by change_index excluded.
  5. Check fee caps:
    • the envelope’s max_fee_per_gas and max_priority_fee_per_gas are at most the signed caps,
    • tx.gas_limit <= max_gas_limit.

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:

  1. For each input, check-and-set its spent bit at the verified index. If it is already set, the frame fails.
  2. If the frame’s payer is zero, set payer = 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:

  1. Debit the vault by each input’s value.
  2. Create each output: increment next_utxo_index, credit the vault, emit UtxoCreated. The change output receives the left-over.
  3. Apply account_outs.
  4. 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.

11 Likes

IIUC that “spent bit” is still a leaf in a Merkle tree, that’s not very different from a 256-bit account balance. I don’t think the UTXO model can save all that much space (or at all).

1 Like

You might be missing this part:

Since UTXOs will get a protocol-assigned index, we can use that index to tick-off specific bits within a word. This is where the savings are coming from, e.g. compared to nullifiers.

Ah, fair enough. That way you’re basically removing log_2(256) = 8 bits from the keys used to look up these spent bits, as those bits are used separately to select the bit within the leaf word. Correct?

Removing 8 levels from a Merkle tree is indeed a large save.

Concerns on the data-availability burden pushed to end users:

  1. Continuous scanning — a user must watch every block and filter UtxoCreated logs under UTXO_VAULT to discover their own UTXOs.
  2. Self-custody of openings — the contract only retains the aggregated batch_root long-term, so the raw opening and its Merkle path must be kept off-chain by the user. Lose it and the UTXO becomes unspendable.

More importantly, once EIP-4444 lands and historical block bodies/receipts are pruned, the “reconstruct from chain” fallback disappears: eth_getLogs won’t find them, and the only recovery path is a full sync-from-genesis + re-execution — not realistic for ordinary users.

In this example, the fee is 0.1 ETH?

Great post. The “declare transitions, don’t execute” rule helped clarify the design for me.

My current understanding is that native UTXO ownership here is intentionally key-only, not account-based. In other words, a UTXO recipient is an address only insofar as it can be authorized by a protocol-verifiable signature scheme: secp256k1, P-256/passkeys, PQ schemes, etc. The UTXO verifier can then check actors, conservation, gas payment, and sponsor repayment by inspecting the declared frame data, without executing recipient code.

If that understanding is correct, then contract accounts are not intended to be spendable UTXO recipients. A ERC-4337 account could create UTXOs, sponsor frames, or receive account-side settlement, but a UTXO whose recipient is the contract account address itself would not be spendable, because there is no private key whose signature verifies to that address.

Is that the intended boundary?

I ask because Ethereum users generally treat “address” as meaning either an EOA or a contract account. So if native UTXOs are deliberately a key-holder primitive rather than an Ethereum-account primitive, I think the proposal should probably say that explicitly and include a mitigation for the contract-recipient footgun especially for counterfactual CREATE2 accounts, where there may be no code at creation time to warn against.

1 Like

True, but there are several ways to improve on that:

  • Replace logs bloom with some more sophisticated filtering, e.g. EIP-7745
  • SSZ’ify the EL, Pureth, EIP-7919.

It never turns unspendable. Users would need to find a node that didn’t aggressively prune history. UTXOs could also be some special history that isn’t pruned, and instead shared somehow.
As long as the chain is available, UTXOs exist too.

1 Like

No, the fee is gas_used * effective_gas_price and the remaining 0.1 ETH is the maximum possible change before deducting the fee. Left the fee unspecified here.

Great post — the “store consumption, prove existence” split and the protocol-assigned index → dense bitfield trick are, I think, the real contributions here; the second one is what makes this pay off where nullifier-style sets can’t. I went through the design carefully and have some questions plus a list of quicker spec-level ones. Numbered for point-by-point replies.

1. History availability under rolling expiry (building on @Po’s point)

For diligent wallets the proof story holds up nicely: the base proof never changes, and after sealing you attach the batch path once — store both and you never need history again. The problem is every other path: “anyone can rebuild from public chain data” assumes bodies/receipts availability, which rolling history expiry is going to remove. Two parts:

a. Where exactly is openings_root_N committed? “Computed by the client at the block boundary, just like the receipts root” suggests a header-level commitment, but the state table only has it as a ring slot in vault storage — which gets overwritten. If the root only ever lives in the ring, reconstructing an old one strictly requires the creation block’s body. Related: the sealing schedule is unspecified; the spec needs the invariant that batches[b] is readable in state no later than the first overwrite of an era-b ring slot, else early-era UTXOs go transiently unprovable.

b. A concrete alternative to “find a node that didn’t prune” (your reply above): make openings a first-class retention domain, independent of general history — separate segment files with their own retention window, Erigon-domain style, same spirit as era1 files for pre-merge data (and consistent with EIP-7864’s separate-syncability-by-type rationale). The retention set can also be scoped: only openings of unspent UTXOs need to stay available; spent ones can be dropped. That makes the mandatory dataset exactly Bitcoin-UTXO-set-shaped — but as cold flat files with delete-on-spend, instead of hot state. The honest accounting then reads: 0 B hot state at creation, ~100 B cold archive while unspent, ~0.3 B hot state forever once spent — still a great trade, but not “0 B”. Is “UTXOs could be some special history that isn’t pruned” intended to become normative along these lines?

2. Privacy: is transparency-for-density fundamental, and is this a stepping stone?

Is the long-term intent to keep this layer transparent and let shielded systems live elsewhere, or is the vault/frame chassis (deposit flow, per-block roots, protocol settlement, journal-external nullification Ă  la EIP-8250) meant as the base a future enshrined shielded pool would reuse, with a separate sparse nullifier domain and the state cost that implies?

3. Contracts and UTXOs: opcode as complement rather than foundation?

I agree the frame has to be the base primitive — the self-funding and sponsorship arguments are convincing. But those arguments rule out the opcode as the only interface, not as a complement. For already-funded contexts, SPEND_UTXO would give contracts first-class UTXO interaction (escrows and bridges consuming UTXOs, Solidity-gated spend conditions), and it would patch the boundary @leekt216 raised: recipient = contract becomes spendable via code instead of unspendable-by-construction (CREATE2/counterfactual footgun included).
The mempool-safety argument doesn’t seem to apply here: opcode spends are ordinary execution transactions, already subject to arbitrary-state invalidation, while the frame path keeps its static validation untouched — and the opcode’s bit-flip can live in the normal revert journal, unlike the frame’s approval-time set.

Was the complement considered and rejected, or just left out of scope?

4. Gas schedule and adversarial state costs

The Costs section counts bytes, but the gas schedule ultimately decides whether the story holds:

  • ~0.3 B/entry assumes dense spending. Worst case — one spent UTXO per 256-index word — a spend forces a fresh 32 B leaf plus stem overhead, ~100Ă— the average. Is the bit-set priced like SSTORE-from-zero on the first touch of a word, or amortized on a density assumption?
  • Per-output settlement work (index increment, vault credit, log, end-of-block tree leaf) is unpriced, zero-value outputs pass VERIFY (only recipient != 0 is checked), and zero-value creations are legal. Since discovery means scanning topics[2] and self-custody of openings is effectively obligatory, dust-minting at a victim’s address is a cheap way to pollute their tracking set — bounded only by gas. Minimum value, or per-output pricing?
  • Settlement “cannot fail” is argued from solvency only, but spent bits are set journal-externally at approval. Presumably intrinsic charging makes gas-starved settlement impossible — the spec should state that invariant explicitly.

Quicker spec-level questions

  • Change semantics. Is a change output mandatory? If absent (or if change_index is out of range — unchecked), the surplus seems to strand in the vault, owed to nobody, since conservation is >= not ==. VERIFY also never checks the change entry’s signed value is actually 0. And the leftover formula differs by mode and is never written down: sponsored change must not subtract the fee (the payer pays it) or the actor is double-charged. What happens if change_index points at the payer’s repayment output?
  • token_address is unproven as written. The leaf is hash(index, source, recipient, value), Tokens says the opening proofs are “unchanged”, yet an input’s token_address is “read from the proven opening”. These can’t all hold — the leaf (and the creation log) need the token field, else a refreshed witness could relabel which per-token conservation bucket an input funds.
  • Settlement ordering. When do a frame’s account_outs land relative to later frames — can a later execution frame in the same transaction spend them (which would enable a neat “fund an execution from a UTXO” pattern), or does settlement run at end of transaction?
  • UTXO_VAULT at the EIP-8141 layer. 8141’s only special target today (the expiry verifier) is code-backed, with native handling just an optimization, and the “canonical paymaster” is mempool policy — a code-free protocol-semantics target is a new class. Extend 8141 with a generic canonical-target mechanism, or hardcode the vault? Also, which phase enforces “at most one UTXO frame per transaction”? (The VERIFY section reads “for each UTXO frame”.)
  • Mempool machinery. Witness refresh + re-wrap means one signed spend circulates under many txids: mempools need a frame-level identity (signed digest / input set) for dedup, plus input-conflict replacement pricing, RBF-style. Losing conflicts pay nobody (“the whole transaction fails and the payer is never charged”), so conflict-flooding is free at the relay layer. Also worth stating: no same-block spends (the root exists only at block end — 1-block minimum latency), and pre-signed spends die on reorgs that re-index the creation, since the signed (index, creation_block) pair goes stale — unlike account transactions, a re-sign is required.
  • Account sweep as an exit ramp. A side-use I’d like to see acknowledged: accounts sweeping their full balance into UTXOs and going quiescent. It can’t delete the leaf (state clearing only removes nonce = 0 empties), but it leaves the easiest possible class for any future expiry scheme and removes the incentive to mint fresh leaves. Would a “drain” semantic (send-all-after-actual-fee) be worth having so sweeps can be exact? Today the gas refund lands back in the account post-execution, so you can’t actually hit zero.
  • Aggregatable signatures. Multi-actor consolidation — N signatures over the same frame digest — is the ideal aggregation case, and EIP-8141 explicitly keeps raw signatures non-introspectable “to allow future aggregation schemes”. Any concrete plan for an aggregate scheme tag, and is there a PQ-compatible aggregation story (BLS aggregates but isn’t PQ; hash-based schemes don’t aggregate)?

Sorry for the wall of text. :folded_hands:
I enjoyed a lot reading this! Nice work

For this question, I saw there is an aggregative proof for XMSS which is a PQ-signature.
Link: GitHub - leanEthereum/leanVM: Minimal zkVM for Ethereum. · GitHub

That say the user who send the frame transaction needs to generate the proof.

This design forces users to either watch the rest of the ring buffer get filled before they can seal a merkle proof to the latest batch and go offline, or have some out-of-band way to fetch the corresponding opening root binary tree when they come back online to spend the UTXO (This is always true for receiving a UTXO while offline).

Assuming a model where the user has no opening tree/merkle path or batch root merkle path to prove a spend, yet it has the block the UTXO was emitted (lets assume the UTXO discovery protocol includes this data)

To get this data, you need to fetch the UTXO logs from that specific block (maybe years ago), as well as the 8192 surrounding opening roots to reconstruct the batch binary tree.

Both of those operations are expensive and generally unsupported by free RPCs (including most if not all standard wallet RPCs), fetching eth_getLogs for a specific block from months or years ago will typically give you an error on 95% of free RPCs. What’s even harder to get is historical storage slots for the 8192 surrounding opening roots to reconstruct the batch tree, there’s basically zero free RPCs that will allow this since its rife for DoS and misuse.

Not saying this is a bad idea or impossible, but its a big burden for nodes and users, so its definitely going to need more thought with regards to how users actually discover and prove these spends. Standardized RPC methods for UTXO discovery and proving are a good idea to be thinking about early.

Thanks a lot for the inputs!

It would be put into a ring buffer, not a header field. The header field would be needed if we wanted to construct proofs against the EIP-2935 block hashes, but since we don’t do that (to keep witnesses small), we can simply write them into the ring only. In addition, there may be benefits in also putting it into the header, thinking of syncing utxos independently from payloads.

I’m thinking of parsing the utxo create logs after execution, similar to consolidations and exits, and storing the openings on disk, independent from every other state. The default is storing utxos, partially-full nodes can prune them and would still need to be able to follow the chain (utxos are not state like in bitcoin, but only the spent_bits are).

Wrt to privacy, I can def tell it works nicely with stealth addresses (e.g. ERC-5564). How it’s described in the post, I think nothing prevents trustless coinjoins but I haven’t gotten deep into that yet.

Yeah the opcode might be a good complement but I’m not sure it’s actually needed or if we can achieve the same with frame transactions already. E.g. spending utxos from a contract is already possible by having a frame with the contract as tx.sender calling the vault. I find it clear to have it always statically inferrable from the data when a utxo-style transition is about to happen.

I think this can be ignored as it’s a rather expensive attack that is not even hurting too much. Users cannot fake/influence the utxo index they’re assigned, thus, spending one bit per word would require doing 256 utxo transaction and then spend one - which seems expensive - especially if you want to fill multiple words

Yeah this is to be figured out. There might be a gap in the logic for sponsored spends but in general the rule should be to send change to the “change_address” or burn it. ETH should never end up stuck in the utxo vault. Maybe a bit just to support it sponsoring transactions, but not a growing amount.

Yeah this would need to change.

I think they should land with the respective transaction, so yeah, would be spendable in the same block.

You only need to refresh the witness once after 8192 blocks in case the UTXO wasn’t spent yet. This feels managable since one can tell how old an utxo is by the creation_block.

This is interesting yeah. Basically a way to even reduce state. Ofc, deleting account can come with other challenges. It’s not simple.

That’s more part of EIP-8141 (Frame Transactions) than the UTXO mechanism but I’m sure there will be PQ signatures that are aggregator. Flock is and example for that allowing us to have frame transactions with pq sigs, and aggregate the verification computation (of each individual sig) into one proof.

Yeah this is fair but on the other hand clients could also bake it into nodes such that openings are stored on disk automatically and provided to other nodes via eth/. So, “continous scanning” sounds like a lot of work but is essentially for free for nodes that follow the chain. Clients could even allow you to only store your own utxos or those you care about.

As long as the transaction that created the UTXO is available, the UTXOs are too. EIP-4444 def impacts the proposal but we could then differentiate between history and openings. While history is pruned, (some) openings are kept around.

I believe some interesting opportunities (i.e. native shielded pool, escrow payments,…) could be possible by adding another param in this UTXO object called data that can be at least 32 bytes of optional arbitrary data. If not used, it defaults to 0.

I shared this suggestion originally for EIP-8182 in ethereum magicians but think its quite applicable here as well because of EIP-8141 frame transactions paired with it.

This is the high-level flow I roughly see for a shielded pool using these native UTXOs with data:

  1. Alice sends UTXO note to shielded pool address as recipient with value of 2 ETH and data of hash(bobs_address, random_value).
  2. Bob now owns 2 ETH inside shielded pool and wants to send 1.5 ETH to Charlie.
  3. Bob generates ZK proof of owning 2 ETH note and create 2 new notes, one to Charlie and excess back to Bob. These 2 notes will be:
    (index, source, recipient, value, data)
    note1 = (index, shielded_pool_address, shielded_pool_address, 0, hash(charlies_address, random, 1.5e18))
    note2 = (index, shielded_pool_address, shielded_pool_address, 0, hash(bobs_address, random, 0.5e18 - gas_fee))
  4. To execute private transfer, shielded pool address uses EIP-8141 frame to verify proof and execute. Since the shielded pool address holds ETH, it can cover any gas fees without needing a relayer. Or that could be sponsored, not sure.
  5. Charlie now owns 1.5 ETH in the shielded pool and withdraws 1 ETH to a stealth address. Same steps as 3 + 4 but instead recipient is stealth address and value is 1 ETH.

For simpler use-cases like escrow payments, Alice could send 1 ETH note to escrow address with data being hash(bobs_address, 1e18, unlock_timestamp_value) (or could be encoded instead of hashed depending on allowed byte size). Then escrow can frame VERIFY a proof that returns valid only if block.timestamp > unlock_timestamp_value && note.recipient == bobs_address && note.value == 1e18.

Would love to know if this has been considered already or open to the idea of it.

I think you get the same properties by just using Stealth Addresses.
One can build extensions to utxo, like the ERC-5564 Announcer that can emit event logs with additional info alongside the UTXO creation log, bundled in the same transaction.
Ofc, this is just additional data that is only used by the recipient to find the respective stealth account, and never used to unlock the utxo. As soon as you combine it with zk snarks, you may need to enshrine a verifier into the protocol which quickly increases surface area, problems and complexity.

It’s not necessarily the user who has to create the batch witness and attach it to the transaction, but instead every node can do so. It’s even cheap.
This works because the witness to the receipt isn’t signed over.

This is true and I agree. The problem is more in poor RPCs than UTXOs though. It must be possible to get that data efficiently without even necessarily revealing my identity. Unfortunately, we, Ethereum, have not been doing the best job on that but things can change.
Tbf, we’re dealing with extreme cases here: regular payments are usually spent/forwarded quickly and not kept for years. Also, nodes could keep utxos (they are fairly small) while still pruning history - this is to be figured out I’d say.

One thing: 1-block minimum spend latency (root only exists at block end) and witness-refresh mechanics create new timing structure that cross-venue latency models would need to account for if payments start flowing this way.

On Bitcoin, a spend references its parent by txid, and the txid exists the moment the transaction is constructed, before it’s in any block. So you can chain transactions in the mempool and have parent and child confirm in the same block (this is what CPFP fee-bumping relies on). The only ordering rule is the parent appears earlier in the block than the child.

The Ethereum proposal can’t do that because it deliberately doesn’t store UTXO openings anywhere in state.

shift custody of critical data from the chain to the user. In Lightning, if you lose your channel state, you can lose funds (or need your counterparty’s cooperation). Here, if you lose your UTXO opening — the source, value, recipient data that’s never stored on-chain — you can’t construct a spend proof. And once EIP-4444 prunes old logs, reconstructing it means digging through archival history providers. That’s the same anti-pattern: “the protocol scales by making you responsible for your own data,” which historically users are bad at. Expect the same ecosystem response too — third-party services (like watchtowers for Lightning) emerging to hold openings for you, which reintroduces trust assumptions the design was trying to avoid.

That’s correct. Since UTXOs must be proven against the block’s post-state, there is a one-block delay between creating and spending a UTXO.

I think this is more nuanced. It’s not a binary choice where all data is either stored by users or by the protocol (i.e. every node). Not all nodes have the same responsibilities.

I’m thinking of different roles in the network: attesters, proposers, builders, includers (FOCIL), RPC providers, state providers, history providers, etc. Even with EIP-4444, a node could prune historical blocks while still extracting UTXOs during sync and retaining only those, without keeping the full history.

So openings would not necessarily have to follow the EIP-4444 pruning rules. And even if we don’t want users to store the openings they need themselves (although it should become super simple to sync a partial stateful node that keeps (certain) UTXOs), there are still many other places to obtain them: RPC providers, a local node, or specialized UTXO providers. Wallets could easily run lightweight services that retain UTXOs. Likewise, partial stateful nodes can follow and verify the chain without storing the UTXOs themselves, keeping only the spent list in state.

1 Like

what’s the incentive model for UTXO providers, and what happens to a user whose opening nobody happened to retain? “It should become super simple to run a partial stateful node” is a capability claim, not an availability guarantee — the failure case isn’t that retention is hard, it’s that it’s nobody’s job.