EIP-8222 - Lean Staking

Created 2026-04-09
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

"Lean Staking" is an L1 native, two-phase staking process providing validator unlinkability. It decouples execution layer (EL) deposit addresses from consensus layer (CL) validator keys by routing deposits through a pending deposit tree. Depositors first submit a pending deposit along with a cryptographic commitment to the staking contract. A separate party then claims the deposit by providing a zero-knowledge proof of commitment ownership, at which point the deposit is added to the validator entry queue.

We first describe a simple version of the protocol that requires only EL changes to the staking contract. Then, in section "Two-phase Withdrawals", we describe a slightly more involved version that requires changes to withdrawal mechanism in CL. However, our changes to the validator withdrawal protocol provides a strong in-protocol primitive for private ETH transfers with novel plausible deniability for sender and recipient of the funds.

Lean Staking can be post-quantum secure provided the underlying proving system is as well. We provide an implementation using LeanVM, an ongoing Ethereum Foundation project, which is envisioned as a formally verified and post-quantum ready proving system. It uses Poseidon1 over the Koala-Bear field (and an extension of degree 5). The current draft does not enforce a specific hash function or field, as both are tied to the chosen proving system.

Motivation

Privacy is a pressing requirement for Ethereum. L1 staking is Ethereum's economic and security powerhouse. At the time of this EIP, the staking contract locks more than 38 million ETH ($81B). However, the current staking flow is public, linking deposit addresses to validator keys. This lack of privacy compromises solo and institutional stakers alike, sometimes putting their funds and even physical safety at risk. Note that the validator needs to have both deposit privacy (not leaking information about their on-chain identity) and network anonymity (not leaking information about their IP); the later is a well-studied problem with mature solutions (e.g. onion routing, mixnet, VPNs).

With EIP-6110, deposit inclusion and validation shifted to the execution layer. This means Lean Staking can be implemented entirely through changes to the staking contract without any consensus layer modifications. Breaking the link between EL and CL credentials would provide a powerful base layer privacy primitive. Because all validators must use the same two-phase deposit mechanism, Lean Staking also enables private value transfers for users who have no intention of running a validator. Such users can submit a pending deposit and claim it to a different address, thereby using an indistinguishable process from genuine validator deposits. This dual use is what makes the construction powerful: validators get privacy by default, while non-validators can leverage the same anonymity set for private transfers with plausible deniability.

Specification

Lean Staking introduces a two-phase staking process:

  1. Pending deposit phase: The depositor sends ETH along with a cryptographic commitment to the staking contract. The commitment is inserted into an append-only pending deposit tree. This transaction is public but reveals nothing about the future validator identity.

  2. Claim phase: A separate party (i.e. relayer or account abstraction (AA) bundler) submits a zero-knowledge proof of knowledge of an unclaimed commitment in the pending deposit tree. Upon successful verification, the deposit is added to the validator entry queue with the provided CL credentials. The commitment is nullified to prevent double-claiming.

The original depositor should not submit the claim transaction directly, as an observer could link his pending deposit and claim phases.

Pending Deposit Tree

We add a "Pending Deposit Tree" to the staking contract. The pending deposit tree is an append-only incremental binary merkle tree, where each leaf stores a 32 bytes value corresponding to a cryptographic commitment. The tree should use a SNARK-friendly hash function like Poseidon.

The on-chain contract only needs to store the current frontier (one hash per tree level) to compute root updates. The staking contract maintains a circular buffer of recent pending deposit tree roots (e.g., the last 1024 roots). A claim is valid against any root in this buffer.

Pending Deposit

To start unlinking with Lean Staking, the depositor must send at least 1 ETH alongside a cryptographic commitment to the staking contract. We update the staking contract to replace deposit method with a add_pending_deposit method that inserts the commitment into the pending deposit tree. The pending deposit phase only commits the ETH; it does not associate it with any validator.

function add_pending_deposit(bytes32 commitment) external payable {
  require(msg.value >= 1 ether);
  bytes32 newRoot = insertLeaf(root, commitment);
  storeRoot(newRoot);
  emit PendingDeposit(commitment, commitmentCount - 1);
}

The commitment is inserted into the pending deposit tree and emits a PendingDeposit event.

zkSNARK

The zkSNARK attests in zero-knowledge to the knowledge of an unclaimed commitment in one of the stored pending deposit tree roots and reveals the corresponding nullifier. The proof is intrinsically bound to the validator credentials and deposit amount because they are part of the commitment preimage, i.e. substituting different credentials produces a different commitment that is not in the tree, preventing front-running (see Claim Front-running).

The commitment and nullifier are defined as:

Where nullifier_preimage is a single random value chosen by the depositor at pending deposit time. It serves as both the hiding randomness for the commitment and the source of the nullifier. An observer cannot link a revealed nullifier to a commitment without knowing the nullifier_preimage.

Claim

A claim transaction consumes a pending deposit commitment and adds the deposit to the deposit tree, and subsequently CL will process it. When claiming, users provide a valid zkSNARK proving knowledge of a non-nullified commitment. The contract records the nullifier to prevent double-claiming.

function claim(
    bytes calldata pubkey,
    bytes calldata withdrawal_credentials,
    bytes calldata signature,
    bytes32 deposit_data_root,
    Proof calldata deposit_proof,
    bytes32 nullifier,
    uint256 amount
) external {
    // verify deposit_proof against (nullifier, pubkey, withdrawal_credentials, amount, root)
    // credentials and amount are public inputs to the circuit — the proof is bound to them
    // because they are part of the commitment preimage
    ...
}

The layout of Proof is determined by the chosen proving system.

Two-phase Withdrawals

We extend the same two-phase pattern to the withdrawal side. When a validator exits, instead of withdrawing directly to a known address, they submit a pending withdrawal with a cryptographic commitment to a "Pending Withdrawal Tree" (analogous to the Pending Deposit Tree). The ETH is then claimed on the execution layer by proving knowledge of the commitment — breaking the link between the validator and the withdrawal recipient.

The pending withdrawal tree has the same structure as the pending deposit tree: append-only incremental merkle tree, circular root buffer, separate nullifier set.

Withdrawal Commitment Credential (type 0x03)

We introduce a new withdrawal credential type 0x03. When a validator sets their withdrawal credentials to a 0x03 credential, the remaining 31 bytes encodes a withdrawal commitment rather than an address. The withdrawal commitment is defined as:

Where withdrawal_nullifier_preimage is a random value chosen by the validator (or the original depositor, in the case of a privacy transfer). When the validator exits, the CL inserts the withdrawal commitment into the pending withdrawal tree instead of sending ETH directly to an address.

This fits naturally into the existing credential type system (0x00 for BLS, 0x01 for execution address, 0x02 for compounding). A 0x03 credential signals that the validator opts into two-phase withdrawal.

Pending withdrawal (CL): When a validator with 0x03 credentials exits, the CL inserts their withdrawal commitment into the pending withdrawal tree.

Claim withdrawal (EL): A party proves knowledge of the withdrawal_nullifier_preimage of an unclaimed withdrawal commitment in the pending withdrawal tree and receives the ETH to the recipient_address embedded in the commitment. The contract records the withdrawal nullifier (hash(withdrawal_nullifier_preimage)) to prevent double-claiming.

Direct Pending Deposit to Pending Withdrawal

To avoid forcing privacy-only users through the full validator lifecycle, we allow pending deposits to be routed directly into the pending withdrawal tree. A user proves ownership of an unclaimed pending deposit commitment and, instead of entering the validator entry queue, provides a withdrawal_commitment that is inserted into the pending withdrawal tree. The user (or a third party) then claims the pending withdrawal on the execution layer.

Note that in this case the deposit-side commitment already binds to a 0x03 withdrawal credential containing the withdrawal_commitment. The depositor chooses both the deposit nullifier_preimage and the withdrawal_nullifier_preimage at deposit time.

This means a privacy user's flow is: (1) submit a pending deposit with commitment (which includes 0x03 withdrawal credentials encoding a withdrawal commitment), (2) prove ownership of the pending deposit and insert the withdrawal commitment into the pending withdrawal tree, (3) claim withdrawal to the recipient address. The user never enters the validator set, avoiding the entry queue, validation duties, and exit queue entirely.

This modification is not required for the core deposit-side protocol to function. Without it, a user can still achieve privacy by entering the validator set and subsequently requesting exit, at the cost of waiting through the entry and exit queues.

Rationale

Root Expiry

The contract stores only the last N roots (e.g., 1024) in a circular buffer. A user's claim whose proof references an evicted root must recompute their proof against a current root. The buffer size should be chosen to give users sufficient time to generate and submit proofs under normal network conditions. Very old roots also correspond to smaller anonymity sets, so expiring them has the secondary benefit of encouraging claims against roots with larger anonymity sets.

Nullifier Set Growth

The staking contract maintains a set of spent nullifiers and rejects any claim with a previously used nullifier. While this prevents double-claiming of pending deposit commitments, this means the nullifier set grows monotonically. Over time this could become a scalability concern for on-chain storage.

Evolving nullifiers, introduced by Bowe and Miers and adopted in the Zcash Tachyon upgrade, offer a solution. The nullifier set is divided into epochs; when an epoch fills or a cap is reached, a fresh set is created. Any nullifier inserted into the new set must be accompanied by a recursive proof that it does not appear in any previous epoch's set. This allows validators to permanently prune old nullifier sets while preserving double-spend protection. Adapting this construction to Lean Staking could bound on-chain nullifier storage and keep claim verification costs constant regardless of how many claims have been processed historically.

Fixed Denominations

Although the amount is bound to the commitment, allowing arbitrary deposit amounts is problematic for privacy: all deposits of the same amount effectively form a separate anonymity set. An unusual amount (e.g., 37.5 ETH) may have very few other deposits to hide among, making linking trivial.

To enforce good privacy hygiene, the contract could restrict deposits to a fixed set of denominations (e.g., 32, 64, 128, 256, 512, 1024, 2048 ETH), with a separate pending deposit tree per denomination. Post-Pectra (EIP-7251), validators may hold effective balances up to 2048 ETH, so supporting only 32 ETH is not practical.

This introduces a tradeoff: more denominations provide flexibility but fragment the anonymity set across multiple trees. Fewer denominations force larger deposits to split across multiple cycles(e.g. a 96 ETH deposit would require a 64 ETH and a 32 ETH pending deposit). The optimal denomination set depends on the observed distribution of validator balances and should concentrate most deposits into a small number of well-populated sets.

Plausibly Deniable Transfers

Lean Staking offers a plausibly deniable value transfer mechanism as a byproduct. Unlike standalone privacy protocols which may be stigmatized or censored, Lean Staking's privacy is embedded in critical Ethereum infrastructure that all stakers must use.

Plausible Deniability with Deposit-side Only

With the deposit-side protocol alone, a depositor can set the receiver's address as the withdrawal_credentials in their commitment. An observer sees a standard staking operation indistinguishable from any other validator deposit. The sender has clear plausible deniability, since he can claim to be a legitimate validator.

The recipient's situation is more nuanced. Unlike the Wormhole construction (EIP-7503) where the recipient mints fresh ETH through a privacy-specific mechanism, here the recipient receives funds through a normal validator withdrawal, a public action that all validators use. In principle this gives the recipient some deniability: they can claim they were simply a validator who exited. However, since a privacy-seeking user is unlikely to actually run a validator for an extended period, they would enter the validator set and immediately request exit. This behavior is unusual: legitimate validators typically run for extended periods and an attentive observer could flag short-lived validators as likely privacy transfers, weakening the recipient's deniability in practice.

Two-sided Plausible Deniability

With two-phase withdrawals, both sides of a private transfer are covered:

An observer sees a pending deposit on one end and a withdrawal claim on the other, with both looking like routine staking operations. There is no public link between them. Neither the sender nor the recipient needs to justify using a "privacy tool" because both are using the same mechanism that all legitimate validators use. With the direct pending-deposit-to-pending-withdrawal path, neither party ever activates as a validator.

To our knowledge, this is the first construction offering two-sided plausible deniability for private transfers on Ethereum.

Reference Implementation

We implement Lean Staking using LeanVM, a VM coupled with a post-quantum proving system based on a combination of WHIR^1 and SuperSpartan. LeanVM exposes a Poseidon1 precompile (poseidon16, where 16 denotes the permutation width — two 8-element Koala-Bear field arrays — not a hash version). Since the commitment preimage exceeds 16 field elements (31 elements total: 8 for nullifier_preimage, 13 for validator_key, 9 for withdrawal_cred, 1 for amount), the commitment hash requires chained poseidon16 calls.

def compute_commitment(nullifier_preimage, validator_key, withdrawal_cred, amount):
    # Memory layout (contiguous):
    #   nullifier_preimage (8) | validator_key (13) | withdrawal_cred (9) | amount (1)
    # Total: 31 field elements → 3 chained poseidon16 calls
    #
    # Call 1: hash(nullifier_preimage[0:8], validator_key[0:8])
    # Call 2: hash(h1, validator_key[8:13] | withdrawal_cred[0:3])  — contiguous at validator_key + 8
    # Call 3: hash(h2, last_block) — explicitly packed: withdrawal_cred[3:9] | amount | 0
    h1 = Array(8)
    poseidon16_compress(nullifier_preimage, validator_key, h1)
    h2 = Array(8)
    poseidon16_compress(h1, validator_key + 8, h2)
    last_block = Array(8)
    wc_tail = withdrawal_cred + 3
    for i in unroll(0, 6):
        last_block[i] = wc_tail[i]
    last_block[6] = amount[0]
    last_block[7] = 0
    commitment = Array(8)
    poseidon16_compress(h2, last_block, commitment)
    return commitment

def compute_nullifier(nullifier_preimage):
    nullifier = Array(8)
    poseidon16_compress(nullifier_preimage, nullifier_preimage, nullifier)
    return nullifier

def dual_mux(a, b, switch):
    # asserts switch is 0 or 1
    assert switch[0] * (1 - switch[0]) == 0
    c = (b - a) * switch[0] + a
    d = (a - b) * switch[0] + b
    return c, d

def verify_path(levels: Const, leaf, leaf_sibling, is_right_child, root):
    node: Mut
    sibling: Mut
    flag: Mut

    path = DynArray([])
    node = Array(8)
    l, r = dual_mux(leaf, leaf_sibling, is_right_child)
    poseidon16_compress(l, r, node)

    sibling = is_right_child + 1
    flag = sibling + 8

    for _ in unroll(0, levels):
        l, r = dual_mux(node, sibling, flag)
        path_node = Array(8)
        poseidon16_compress(l, r, path_node)
        sibling = flag + 1
        flag = sibling + 8
        node = path_node
        path.push(node)

    final_node = path[levels - 1]
    for i in unroll(0, 8):
        assert root[i] == final_node[i]
    return

# program constants
VALIDATOR_KEY_SIZE = 13
WITHDRAWAL_KEY_SIZE = 9
NULLIFIER_SIZE = 8
ROOT_SIZE = 8
NULLIFIER_PREIMAGE_SIZE = 8


# located at (NONRESERVED_PROGRAM_INPUT_START + inputs.public_inputs.len()).next_power_of_two()
NONRESERVED_PROGRAM_PRIVATE_INPUT_START = 128

def main():
    levels = 32

    # public inputs
    validator_key = NONRESERVED_PROGRAM_INPUT_START
    withdrawal_cred = validator_key + VALIDATOR_KEY_SIZE
    amount = withdrawal_cred + WITHDRAWAL_KEY_SIZE
    nullifier = amount + 1
    root = nullifier + NULLIFIER_SIZE

    # private inputs
    nullifier_preimage = NONRESERVED_PROGRAM_PRIVATE_INPUT_START 
    leaf_sibling = nullifier_preimage + NULLIFIER_PREIMAGE_SIZE
    leaf_is_right_child = leaf_sibling + 8

    commitment = compute_commitment(
        nullifier_preimage, validator_key, withdrawal_cred, amount)
    computed_nullifier = compute_nullifier(nullifier_preimage)
    verify_path(levels, commitment, leaf_sibling, leaf_is_right_child, root)

    for i in range(0, NULLIFIER_SIZE):
        assert nullifier[i] == computed_nullifier[i]

    return

Benchmarks

We run preliminary benchmarks using LeanVM for different $\rho$ values. We assume a deposit commitment tree of depth 32, a depth that should accomodate all validator entries for a sizeable amount of time. We report average proving time, proof sizes and verification times for generating claim proofs on LeanVM, running on a Macbook M1 Pro (N = 500). The benchmark code is vendored as an auxiliary asset: see leanstaking.

Rho Proving Time Verification Time Proof Size
1 0.035s 0.012s ~189KB
2 0.032s 0.008s ~124KB
3 0.035s 0.006s ~99KB
4 0.041s 0.005s ~83KB

Security Considerations

Compatibility with EIP-7002

Because the withdrawal credentials of validators remain hidden, the introduced type 0x03 withdrawal credentials in its current form is not compatible with EIP-7002. One way to fix this issue is separating the withdrawal and exit authority addresses, and keeping withdrawal address in the hiding commitment while making exit authority address public. EIP-7002 type requests originated from an exit authority address of a validator would be able to trigger an exit like before. Note that users that don't need smart contract triggered exit can set the exit authority address to 0x0 and use voluntary exit messages to leave the active validator set.

Compatibility with Non-Custodial Staking

Non-custodial staking makes up a large portion of total staking on Ethereum. The current version of EIP is fully compatible with solutions that rely on (a) vulontary exits (i.e. pre-signed exit messages), and/or (b) require node operators to post an initial pre-deposit. This covers almost all of todays major delegated staking protocols.

Deposit Privacy vs Network Anonimity

To preserve the privacy of a validator, it is necessary for to have both deposit privacy (not leaking information about their on-chain identity) and network anonimity (not leaking information about their IP). Note that the best network anonymity solution without private deposits could fall short to preserve privacy of a validator simply by revealing their on-chain footprint.

Proof Soundness and Post-quantum Security

The security of Lean Staking depends on the soundness of the underlying proving system. A broken proof system would allow an attacker to claim deposits without valid commitments. The choice of LeanVM and its formal verification effort is motivated by this critical dependency.

Linking via Transaction Timing

An adversary who observes both the pending deposit and claim transactions may attempt to correlate them via timing analysis. Users should wait for a significant number of additional commitments to be added to the pending deposit tree before claiming, to increase the anonymity set. The delay between the pending deposit and claim should be variable and unpredictable.

Claim Front-running

A mempool observer could attempt to front-run a claim transaction by substituting their own pubkey and withdrawal_credentials. This attack is prevented by the commitment structure: since commitment = hash(nullifier_preimage | validator_key | withdrawal_cred | amount), the validator credentials and amount are part of the commitment preimage. The circuit takes the credentials as public inputs and uses them to recompute the commitment, which must match a leaf in the pending deposit tree. Substituting different credentials produces a different commitment that is not in the tree, causing proof verification to fail.

Copyright

Copyright and related rights waived via CC0.

{
    "type": "paper-conference",
    "id": "https://eprint.iacr.org/2024/1586",
    "title": "WHIR: Reed-Solomon Proximity Testing with Super-Fast Verification",
    "author": [
        { "given": "Gal", "family": "Arnon" },
        { "given": "Alessandro", "family": "Chiesa" },
        { "given": "Giacomo", "family": "Fenzi" },
        { "given": "Eylon", "family": "Yogev" }
    ],
    "issued": { "date-parts": [[2024]] },
    "URL": "https://eprint.iacr.org/archive/2024/1586/1732176975.pdf",
    "custom": {
        "additional-urls": [ "https://eprint.iacr.org/2024/1586" ]
    },
    "DOI": "10.1007/978-3-031-91134-7_8"
}
```