EIP-8141: Frame Transaction

Hello! I have a quick question in the context of the early feedback for EIP-7906.
One of the main reasons that EIP had to explicitly introduce a new POST_TX frame is to allow a “validation failed, state changes reverted, gas fee still paid” behaviour, which appears somewhat similar to what has been proposed as gas “Guarantor” mode in PR #11555 and later PR #11681, allowing the sender’s VERIFY frame to fail at any time.

Is there still any interest in introducing this or some comparable mechanism in EIP-8141?
It seems that even without EIP-7906, there could be a benefit in having a frame that can confidently revert all transaction changes across all atomic batches.
It might also allow EIP-7906 to focus on exposing the state writes journal, without trying to extend EIP-8141 with the functionality that it needs from it.

Thank you.

This draft looks great, and I’m glad to see Ethereum moving off of hard-wired ECDSA signatures. One thing I’d like to add to the discussion.

The Security Considerations section already flags Arbitrary Signature Malleability:

When an ARBITRARY signature has empty msg, its raw bytes are elided from the canonical signature hash and can therefore be changed without changing TXPARAM(0x08). […] Custom verifiers should enforce canonical encodings and reject unused bytes.

— EIP-8141, Security Considerations

That section frames the risk as transaction-hash malleability. I want to point out a second consequence it doesn’t discuss: those same elided bytes are still billed, so the malleability can be used to grief the payer. This is related to the malleability of VERIFY frame data that @thegaram33 raised earlier:

the input data to the sponsor is intentionally left malleable […] this malleability seems quite risky

@thegaram33, post #21

The billing gap. In existing transaction types the signer commits to a transaction-level gas limit, so a signer bounds their own gas exposure. Frame transactions have per-frame gas_limits but no signed transaction-level gas limit, and gas is computed over the whole transaction. In particular, standard_gas_limit includes:

signature_data_cost = sum(
    calldata_cost(sig.signer) + calldata_cost(sig.msg) + calldata_cost(sig.signature)
    for sig in tx.signatures
)

so the raw signature bytes are billed as calldata, but compute_sig_hash elides those bytes whenever msg is empty, as @nlordell noted:

the current spec only seems to omit signature bytes from signature frames where msg = 0

@nlordell, post #163

(the specific rule is if len(sig.msg) == 0: sig.signature = Bytes() before hashing). So part of what determines the payer’s charge (the elided bytes of an empty-msg entry) is not covered by the signature hash and can be changed without invalidating the signature.

The attack. Given a frame transaction that carries an empty-msg ARBITRARY entry, a party in the transaction’s path can pad that entry’s signature bytes. The protocol performs no cryptographic check on ARBITRARY signatures, so this passes protocol validation and leaves compute_sig_hash unchanged, but it inflates signature_data_cost, and the extra gas is collected from the payer (this could be the sender, or the sponsor). Fixed-length schemes (secp256k1, P256) are length-validated, so their elided bytes can’t be padded, but the ARBITRARY scheme has no length bound, so an attacker can pad these bytes.

Although the protocol allows this attack, it is possible to mitigate it at the contract level. If the transaction’s verifier (specified in the VERIFY frame) follows the spec’s advice and rejects unused bytes (checking SIGPARAM(0x03) = len(signature)), the padded transaction is simply invalid.

But the attack works if the verifier simply looks at a fixed-length prefix and authenticates without asserting the total length. It doesn’t seem realistic to think that every user-deployed verifier will remember to check the signature length in the contract.

A realistic attacker is a malicious block builder or a relay. A malicious builder would actually collect priority fee on the inflated gas_used.

I see a few simple ways to fix this.

  • (a) Change the recommendation to “reject unused bytes” from advice to a hard requirement. Even if it’s a “requirement” I assume some people will get it wrong.
  • (b) Commit to len(sig.signature) in compute_sig_hash even when the bytes themselves are elided, so padding changes the sig hash.
  • (c) Add a signed transaction-level gas_limit (as in every other tx type), with validity gas_limit >= computed total. This is the most like traditional Ethereum transactions, but it might cause issues with multi-party flows where a sponsor/paymaster signature is appended after the sender signs. If the sender can’t predict the added calldata size the sender would need to set a very high gas_limit.

I’d be curious to hear what everyone else thinks.