Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-8175: Composable Transaction

An extensible EIP-2718 transaction type with separated signatures, fee delegation via fee_auth, and opcodes for signature introspection

Authors Dragan Rakita (@rakita)
Created 2026-02-26
Discussion Link https://ethereum-magicians.org/t/eip-8175-composable-transaction/27850
Requires EIP-2, EIP-1559, EIP-2718

Abstract

This EIP introduces a new EIP-2718 transaction type with EIP-1559 gas fields, typed capabilities (CALL and CREATE) that define the transaction’s operations, a separate signatures list for authentication, a fee_auth field that executes account code to sponsor gas, and three new opcodes — RETURNETH, SIG, and SIGHASH — for fee delegation and in-EVM signature introspection.

Motivation

Each Ethereum upgrade has introduced new transaction types for new capabilities: EIP-1559 for priority fees, EIP-4844 for blobs, and EIP-7702 for authorizations. Both EIP-4844 and EIP-7702 extend and reuse mostly the fields from EIP-1559, yet each required an entirely new transaction type. This leads to linear growth of transaction types with overlapping gas-payment semantics. This EIP proposes a single extensible transaction format where new features are added as typed capabilities without defining new transaction types.

Transaction sponsorship — where a third party pays gas on behalf of the sender — has been a long-sought feature. EIP-8141 proposes a solution using execution frames, new opcodes (APPROVE, TXPARAM), and per-frame gas budgets. This EIP achieves sponsorship with a simpler fee_auth field and three focused opcodes.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Parameters

Parameter Value
COMPOSABLE_TX_TYPE 0x05
CAP_CALL 0x01
CAP_CREATE 0x02
SIG_SECP256K1 0x01
SIG_ED25519 0x02
ROLE_SENDER 0x00
ROLE_PAYER 0x01
RETURNETH_OPCODE 0xf6
SIG_OPCODE 0xf7
SIGHASH_OPCODE 0xf8
RETURNETH_GAS 2
SIG_BASE_GAS 2
SIGHASH_GAS 2
PER_SIG_SECP256K1_GAS 3000
PER_SIG_ED25519_GAS 3000

Composable transaction

A new EIP-2718 transaction is introduced where the TransactionType is COMPOSABLE_TX_TYPE and the TransactionPayload is the RLP serialization of:

rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas,
     gas_limit, fee_auth, capabilities, signatures])

The fields chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, and gas_limit follow the same semantics as EIP-1559.

The fee_auth field is either empty (zero bytes) or a 20-byte address. When non-empty, it designates the account whose code is executed to provide ETH covering the transaction fee (see Fee Delegation via fee_auth).

The capabilities field is an RLP list of typed capabilities. Each capability is an RLP list whose first element is the capability type identifier. The transaction MUST contain at least one capability. See Capability types for the defined types. Future EIPs MAY define additional capability types for this transaction format.

The signatures field is an RLP list of typed signatures. Each signature authenticates the transaction using a per-role signing hash (see Signing Hash). Every signature type MUST be known and every signature MUST be cryptographically valid.

The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).

Capability types

CALL (CAP_CALL = 0x01)

Executes a message call to an existing account.

Format: [cap_type, to, value, data]

Field Description
cap_type 0x01
to 20-byte destination address
value ETH value in wei to transfer
data Calldata bytes

CREATE (CAP_CREATE = 0x02)

Creates a new contract.

Format: [cap_type, value, data]

Field Description
cap_type 0x02
value ETH value in wei to endow the new contract
data Initialization code (initcode)

The contract address is derived as keccak256(rlp([sender_address, nonce]))[12:], where nonce is the sender’s nonce at the time the CREATE capability executes. The sender’s nonce is incremented by 1 for each CREATE capability.

Signatures

Signature schemes

SECP256K1 (SIG_SECP256K1 = 0x01):

Format: [signature_type, role, y_parity, r, s]

Field Size Description
signature_type 1 byte 0x01
role 1 byte ROLE_SENDER or ROLE_PAYER
y_parity 1 byte Recovery ID (0 or 1)
r 32 bytes ECDSA r component
s 32 bytes ECDSA s component

The s value MUST be less than or equal to secp256k1n/2, as specified in EIP-2. The signer address is recovered via ecrecover(sig_message, y_parity, r, s).

ED25519 (SIG_ED25519 = 0x02):

Format: [signature_type, role, public_key, signature]

Field Size Description
signature_type 1 byte 0x02
role 1 byte ROLE_SENDER or ROLE_PAYER
public_key 32 bytes Ed25519 public key
signature 64 bytes Ed25519 signature (RFC 8032, pure Ed25519)

The signer address is keccak256(public_key)[12:]. The signature MUST verify under RFC 8032 pure Ed25519.

Signing hash

The signing hash binds both the transaction content and the signer’s role, providing domain separation. It is computed in two steps:

def compute_base_hash(tx: ComposableTx) -> bytes:
    return keccak256(COMPOSABLE_TX_TYPE || rlp([
        tx.chain_id,
        tx.nonce,
        tx.max_priority_fee_per_gas,
        tx.max_fee_per_gas,
        tx.gas_limit,
        tx.fee_auth,
        tx.capabilities,
        []  # signatures array blinded
    ]))

def compute_sig_message(tx: ComposableTx, signature_type: int, role: int) -> bytes:
    return keccak256(
        b"\x19ComposableTxSig" ||
        bytes1(signature_type) ||
        bytes1(role) ||
        compute_base_hash(tx)
    )

The signatures array is replaced with an empty list when computing base_hash. This allows all signers to sign independently and in any order. The signature_type and role are included in sig_message to prevent cross-scheme and cross-role signature confusion.

Fee delegation via fee_auth

When fee_auth is non-empty, the account at fee_auth sponsors the transaction fee. Before the main transaction executes, the protocol invokes fee_auth as a prelude call:

Context field Value
ADDRESS fee_auth
CALLER sender (recovered from sender signature)
ORIGIN sender
CALLVALUE 0
CALLDATA empty
GAS gas_limit minus intrinsic gas

The fee_auth code MUST use RETURNETH to credit ETH into the transaction-scoped fee escrow. The amount credited MUST be at least gas_limit * max_fee_per_gas (the maximum possible fee). The fee_auth code MAY use SIG and SIGHASH to introspect signatures for authorization.

If fee_auth execution reverts, or the fee escrow is insufficient after fee_auth returns, the transaction is invalid and MUST NOT be included in a block.

After the main transaction completes, the actual fee is settled:

actual_fee = gas_used * effective_gas_price
surplus = fee_escrow - actual_fee
# surplus is refunded to fee_auth address

When fee_auth is empty, gas is charged directly from the sender’s balance, following standard EIP-1559 behavior.

State changes made during fee_auth execution persist regardless of whether the main transaction reverts. This allows sponsors to maintain their own accounting (e.g., nonces, rate limits) independently.

New opcodes

RETURNETH (0xf6)

Returns ETH from the current execution context to the parent (calling) context.

   
Stack input value — amount in wei
Stack output (none)
Gas RETURNETH_GAS (2)

Behavior:

  • Debits value wei from the balance of ADDRESS (the currently executing account).
  • Credits value wei to the parent calling context. If the parent context is a contract call, the ETH is credited to the caller’s account balance. If the parent context is the protocol-level fee_auth prelude, the ETH is credited to the transaction-scoped fee escrow.
  • If ADDRESS has insufficient balance, execution reverts.
  • RETURNETH is valid in any execution context (not restricted to fee_auth), including descendant calls. When used in nested calls within fee_auth, the ETH propagates upward: each RETURNETH credits the immediate parent, and the top-level fee_auth frame’s RETURNETH credits the fee escrow.
  • RETURNETH is invalid in a STATICCALL context and causes an exceptional halt.
  • If the enclosing call frame reverts, the RETURNETH credit is rolled back.

SIG (0xf7)

Loads a signature from the transaction’s signatures array into memory.

   
Stack input index, mem_start
Stack output sig_type
Gas SIG_BASE_GAS (2) + memory expansion cost
Stack position Value
top - 0 index — zero-based index into signatures
top - 1 mem_start — memory offset to write signature data

Behavior:

  • If index >= len(tx.signatures), execution reverts.
  • Pushes the signature_type identifier onto the stack.
  • Writes the signature body (excluding signature_type) to memory at mem_start:
    • SIG_SECP256K1: writes role ‖ y_parity ‖ r ‖ s (1 + 1 + 32 + 32 = 66 bytes).
    • SIG_ED25519: writes role ‖ public_key ‖ signature (1 + 32 + 64 = 97 bytes).
  • Memory expansion cost is charged for the bytes written, following standard rules.

SIGHASH (0xf8)

Pushes the transaction base hash onto the stack.

   
Stack input (none)
Stack output hash — 32-byte base_hash as defined in Signing Hash
Gas SIGHASH_GAS (2)

Behavior:

  • Pushes compute_base_hash(tx) onto the stack as a 256-bit value.
  • The fee_auth code can reconstruct sig_message from base_hash + signature data obtained via SIG, then verify signatures using the ecrecover precompile or future Ed25519 precompiles.

Execution order

The state transition for a Composable transaction proceeds as follows:

def process_composable_tx(tx, block):
    # 1. Static validation
    validate_static_constraints(tx)

    # 2. Compute base hash
    base_hash = compute_base_hash(tx)

    # 3. Validate all signatures and recover addresses
    sender_address = None
    payer_address = None
    for sig in tx.signatures:
        sig_msg = compute_sig_message(tx, sig.signature_type, sig.role)
        addr = recover_address(sig, sig_msg)
        if sig.role == ROLE_SENDER:
            sender_address = addr
        elif sig.role == ROLE_PAYER:
            payer_address = addr

    # 4. Nonce check and increment
    assert state[sender_address].nonce == tx.nonce
    state[sender_address].nonce += 1

    # 5. Intrinsic gas
    intrinsic_gas = compute_intrinsic_gas(tx)
    assert tx.gas_limit >= intrinsic_gas
    gas_remaining = tx.gas_limit - intrinsic_gas

    # 6. Fee handling
    fee_escrow = 0
    max_tx_cost = tx.gas_limit * tx.max_fee_per_gas

    if tx.fee_auth:
        # Execute fee_auth prelude
        fee_auth_result = evm_call(
            caller=sender_address,
            address=tx.fee_auth,
            value=0,
            data=b'',
            gas=gas_remaining,
        )
        gas_remaining -= fee_auth_result.gas_used
        assert not fee_auth_result.reverted
        assert fee_escrow >= max_tx_cost  # accumulated via RETURNETH
    else:
        # Standard: charge sender (or payer if present)
        charge_account = payer_address or sender_address
        assert state[charge_account].balance >= max_tx_cost
        state[charge_account].balance -= max_tx_cost
        fee_escrow = max_tx_cost

    # 7. Execute capabilities sequentially
    for cap in tx.capabilities:
        if cap.cap_type == CAP_CALL:
            result = evm_call(
                caller=sender_address,
                address=cap.to,
                value=cap.value,
                data=cap.data,
                gas=gas_remaining,
            )
        elif cap.cap_type == CAP_CREATE:
            result = evm_create(
                caller=sender_address,
                value=cap.value,
                initcode=cap.data,
                gas=gas_remaining,
            )
        gas_remaining = result.gas_remaining
        if result.reverted:
            break
    gas_used = tx.gas_limit - gas_remaining

    # 8. Settle fees
    effective_gas_price = min(
        tx.max_fee_per_gas,
        tx.max_priority_fee_per_gas + block.base_fee_per_gas
    )
    actual_fee = gas_used * effective_gas_price
    surplus = fee_escrow - actual_fee
    block.coinbase.balance += gas_used * (effective_gas_price - block.base_fee_per_gas)
    # base fee portion is burned

    # Refund surplus
    if tx.fee_auth:
        state[tx.fee_auth].balance += surplus
    else:
        refund_account = payer_address or sender_address
        state[refund_account].balance += surplus

Gas handling

The intrinsic gas cost is:

def compute_intrinsic_gas(tx):
    gas = 21000  # base transaction cost
    for cap in tx.capabilities:
        gas += calldata_cost(cap.data)  # 16 per non-zero byte, 4 per zero byte
        if cap.cap_type == CAP_CREATE:
            gas += 32000  # contract creation cost
    gas += sum(
        PER_SIG_SECP256K1_GAS if s.signature_type == SIG_SECP256K1
        else PER_SIG_ED25519_GAS
        for s in tx.signatures
    )
    return gas

If fee_auth is non-empty, gas consumed by fee_auth execution is subtracted from gas_limit before capabilities execute.

Rationale

Linear growth of transaction types

Rather than defining a new transaction type per feature (blobs, authorizations, sponsorship), each potentially combined with each other, this EIP defines a single extensible format. New features are expressed as capabilities within the existing type.

Separating signatures from capabilities

Placing signatures in their own array cleanly separates extension data from authentication. The signatures array is blinded during hash computation, so all signers produce their signature independently. This eliminates the ordered commitment chain of prior designs and simplifies multi-party signing flows.

Domain-separated signing hash

The sig_message includes signature_type and role alongside the blinded transaction hash. This prevents a sender’s signature from being reused as a payer’s signature, even though both sign over the same base_hash. Without this, an attacker could reinterpret one role’s signature as another’s.

fee_auth for fee delegation

The fee_auth field provides programmable fee delegation. The sponsor’s code runs before the main transaction, uses RETURNETH to escrow the maximum fee, and can implement arbitrary authorization logic by introspecting signatures via SIG and SIGHASH. This is strictly more powerful than a static payer co-signature.

Upfront maximum cost escrow

Requiring fee_auth to escrow gas_limit * max_fee_per_gas before main execution avoids the chicken-and-egg problem of paying for execution with the proceeds of that execution. Surplus is refunded to fee_auth after settlement, mirroring EIP-1559 reserve-and-refund discipline.

fee_auth state persistence

State changes made during fee_auth execution persist even if the main transaction reverts. This allows sponsors to maintain internal accounting (nonces, rate limits, spend tracking) that must not be rolled back on user-transaction failure.

RETURNETH opcode

RETURNETH provides a safe, explicit mechanism for code to return ETH to the parent calling context. When used within the fee_auth prelude, ETH propagates up to the protocol-managed fee escrow. When used in regular calls, ETH is credited to the caller’s account balance. This general-purpose design avoids the need for SELFDESTRUCT or value-carrying calls back to a system address, and enables composable ETH flows beyond fee delegation.

SIG and SIGHASH opcodes

These opcodes enable in-EVM signature introspection. The fee_auth code loads signatures via SIG, obtains the base hash via SIGHASH, reconstructs sig_message, and verifies signatures using ecrecover. This allows arbitrary sponsor authorization without off-chain coordination beyond collecting signatures.

CALL and CREATE as capabilities

Rather than embedding to, value, and data as top-level transaction fields, these are expressed as typed capabilities. This makes the transaction envelope a pure fee-paying and authentication container, while the actual operations — message calls and contract creation — are composable payload items. A transaction may contain multiple capabilities, enabling batched execution within a single transaction. Future capability types (e.g., blob data, authorization lists) extend the same list without modifying the envelope format.

Backwards Compatibility

This EIP introduces a new transaction type and does not modify the behavior of existing transaction types. No backward compatibility issues are expected.

Security Considerations

Signature domain separation

The sig_message includes both signature_type and role, preventing cross-role and cross-scheme confusion. A SECP256K1 sender signature cannot be replayed as an ED25519 payer signature or vice versa.

Payer replay protection

Every signature commits to the full transaction content including the sender’s nonce. Since the sender’s nonce is incremented on each transaction, signatures cannot be replayed across transactions.

fee_auth execution safety

The fee_auth execution is bounded by gas_limit and runs before the main transaction. If fee_auth reverts or credits insufficient ETH, the transaction is invalid and produces no state changes. The upfront escrow requirement (gas_limit * max_fee_per_gas) ensures the protocol never under-collects fees.

fee_auth griefing

A fee_auth contract could consume gas without crediting sufficient ETH, making the transaction invalid. Block builders can simulate fee_auth before inclusion to avoid wasting block space. Mempool nodes SHOULD simulate the fee_auth prelude before accepting the transaction.

RETURNETH restrictions

RETURNETH credits ETH to the immediate parent calling context — either the caller’s account balance or the protocol fee escrow at the top-level fee_auth frame. It cannot send ETH to arbitrary addresses. It is invalid in STATICCALL contexts. If the enclosing call reverts, the credit is rolled back, preventing double-spending.

Transaction propagation

Composable transactions with fee_auth introduce validation complexity similar to EIP-7702 and EIP-8141. Nodes SHOULD keep at most one pending Composable transaction per sender in the public mempool. fee_auth simulation during mempool validation SHOULD be bounded in gas and restricted in state access patterns to limit DoS surface.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Dragan Rakita (@rakita), "EIP-8175: Composable Transaction [DRAFT]," Ethereum Improvement Proposals, no. 8175, February 2026. Available: https://eips.ethereum.org/EIPS/eip-8175.