Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
15215c8
Draft ADR for typed sponsorship transactions
randygrok Jan 5, 2026
a531fc6
Add SponsorTransaction type
randygrok Jan 5, 2026
fd31a25
Revert "Add SponsorTransaction type"
randygrok Jan 5, 2026
9682332
Update ADR for standard-signed typed tx
randygrok Jan 7, 2026
ef4dee4
Expand ADR with payload signing details
randygrok Jan 7, 2026
6be2a11
Document sponsorship validation locations
randygrok Jan 7, 2026
f0f5b6f
Document fee payer execution hook
randygrok Jan 7, 2026
be828f2
update
randygrok Jan 8, 2026
eb43e12
update all the spec steps
randygrok Jan 8, 2026
5a7b861
write reference for tempo
randygrok Jan 8, 2026
cea74f6
remove file
randygrok Jan 8, 2026
0f6398b
update snippet for transaction signature
randygrok Jan 8, 2026
c7eb06a
use txkind to support also contracts
randygrok Jan 8, 2026
a391aea
clarify is not the only transaction
randygrok Jan 8, 2026
29b239f
remove some text
randygrok Jan 8, 2026
59aa4cf
simplify step 1
randygrok Jan 8, 2026
ef04cff
simplify step 2
randygrok Jan 8, 2026
775ce82
Remove fee token from sponsorship ADR
randygrok Jan 9, 2026
73eb0ce
Document dual-domain sponsorship signatures
randygrok Jan 9, 2026
00e71ed
Clarify no pool usage for sponsorship tx
randygrok Jan 9, 2026
9eafb5b
Define executor sender semantics and RPC exposure
randygrok Jan 9, 2026
e33f65f
Update ADR for local typed sponsorship tx
randygrok Jan 9, 2026
6d4330d
Align sponsorship ADR with custom primitives approach
randygrok Jan 9, 2026
1df493d
Clarify sponsor signature semantics
randygrok Jan 9, 2026
3995eaf
docs: clarify txpoolExt_getTxs usage by ev-node
randygrok Jan 12, 2026
869ced7
docs: add spec section for typed sponsorship tx
randygrok Jan 12, 2026
897a338
Clarify txpool, engine, and forced inclusion validation for 0x76
randygrok Jan 12, 2026
d867aaf
Refine sponsorship ADR wording
randygrok Jan 12, 2026
b660754
Clarify persistence and remove ADR repetition
randygrok Jan 12, 2026
e4243b1
Clarify ADR layer scope
randygrok Jan 12, 2026
99002f2
simplify spec
randygrok Jan 12, 2026
de884b9
include batch calls
randygrok Jan 13, 2026
f82b026
Merge branch 'main' into randygrok/typed-tx-adr
randygrok Jan 13, 2026
3a33a93
clean adr
randygrok Jan 13, 2026
f05e5a8
Clarify executor signature envelope in ADR 0003
randygrok Jan 13, 2026
ae3f693
Note executor signature envelope in implementation strategy
randygrok Jan 13, 2026
db46ae0
Clarify signature domain separators and sponsor signature encoding
randygrok Jan 13, 2026
df0b6ca
Update ADR 0003 clarifications
randygrok Jan 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ Custom RPC namespace `txpoolExt` that provides:
- Configurable byte limit for transaction retrieval (default: 1.98 MB)
- Efficient iteration that stops when reaching the byte limit

Note: ev-node uses this endpoint indirectly. It pulls pending txs via `txpoolExt_getTxs`,
then injects those RLP bytes into Engine API payload attributes (`transactions`) for block
building. This means ev-reth's txpool is not used for block construction directly, but it
is used as a source of transactions.

### 6. Base Fee Redirect

On vanilla Ethereum, EIP-1559 burns the base fee. For custom networks, ev-reth can redirect the base fee to a designated address:
Expand Down
194 changes: 194 additions & 0 deletions docs/adr/ADR-0003-typed-transactions-sponsorship.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# ADR 0003: Typed Transactions for Sponsorship and Batch Calls (Type 0x76)

## Changelog
* 2026-01-05: Initial draft structure.

## Status
DRAFT — Not Implemented

## Abstract

This ADR proposes a new EIP-2718 typed transaction (0x76) for the EvNode protocol. The transaction natively supports **gas sponsorship** and **batch calls**. Sponsorship separates the `executor` (identity/nonce provider) from the `fee_payer` (gas provider). Batch calls allow multiple operations to execute **atomically** within a single transaction. This removes the need for off-chain relayers or batching contracts while remaining compatible with Reth's modular architecture.

## Context

Gas sponsorship is a recurring requirement for onboarding users and for product flows that should not require the end user to hold native funds. Today, the only available approaches in Reth are:
1. **Smart Contract Wallets (ERC-4337):** High gas overhead and complexity.
2. **Meta-transactions (EIP-712):** Requires specific contract support on the destination.

EvNode aims to support sponsorship and batch calls natively. We require a mechanism where a transaction can carry two signatures (authorization + payment) and multiple calls, with deterministic encoding and atomic execution.

Terminology: the **executor** is the signer of domain `0x76`; it provides the `nonce`, is the transaction `from`, and maps to `tx.origin`. The **sponsor** (aka `fee_payer`) is the signer of domain `0x78` and pays gas when sponsorship is present; `fee_payer` is the sponsor address field. **Sponsorship** means `fee_payer` is present and pays gas; it does not change the `from`.

## Decision

We will implement a custom EIP-2718 transaction type `0x76` (`EvNodeTransaction`) that encodes **batched calls** plus an optional sponsor authorization.

**Key Architectural Decisions:**

1. **Dual Signature Scheme:** The transaction supports two signature domains. The Executor signature authorizes the action; the Sponsor signature authorizes the gas payment.
2. **Sponsor Malleability (Open Sponsorship):** The Executor signs a preimage with an *empty* sponsor field. This allows **any** sponsor to pick up a signed intent and sponsor it.
3. **Batch Calls are Atomic:** All calls succeed or the entire transaction reverts; there are no partial successes.
4. **Reth Integration:** We will use the `NodeTypes` trait system to inject this primitive. We will not fork `reth-transaction-pool` but will implement a custom `TransactionValidator` to verify sponsor signatures at ingress.
5. **Persistence:** 0x76 transactions are persisted as part of block bodies using a custom envelope in `EthStorage`.

## Specification

### Transaction Structure

**Type Byte:** `0x76`

The **payload** contains the following fields, RLP encoded. Field order is consensus-critical:

```rust
pub struct EvNodeTransaction {
// EIP-1559-like fields
pub chain_id: u64,
pub nonce: u64,
pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub gas_limit: u64,
pub calls: Vec<Call>,
pub access_list: AccessList,
// Sponsorship Extensions (Optional)
pub fee_payer: Option<Address>,
pub fee_payer_signature: Option<Signature>,
}

pub struct Call {
pub to: TxKind,
pub value: U256,
pub input: Bytes,
}
```

The **signed envelope** is a standard typed-transaction envelope with the executor signature:

```rust
pub type EvNodeSignedTx = Signed<EvNodeTransaction>;
```

### Encoding (RLP)

Optional fields MUST be encoded deterministically:

* `fee_payer`: encode `0x80` (nil) when `None`.
* `fee_payer_signature`: encode `0x80` (nil) when `None`.

The `calls` field is an RLP list of `Call` structs, each encoded as:

```
rlp([to, value, input])
```

**Signed envelope encoding (executor signature):**
* The final encoded transaction is `0x76 || rlp([payload_fields..., v, r, s])`, matching EIP-1559-style typed tx encoding.
* The `payload_fields...` are exactly the fields in `EvNodeTransaction` above, in order.

### Signatures and Hashing

This transaction uses two signature domains to prevent collisions and enable the "Open Sponsorship" model. These domain bytes (`0x76` and `0x78`) are signature domain separators, not transaction types.

1. **Executor Signature** (Domain `0x76`)
* Preimage: `0x76 || rlp(payload_fields...)` (no `v,r,s` in the RLP).
* Constraint: `fee_payer` and `fee_payer_signature` MUST be set to `0x80` (empty) in the RLP stream for this hash.
* *Effect:* The executor authorizes the intent regardless of who pays.

2. **Sponsor Signature** (Domain `0x78`)
* Preimage: `0x78 || rlp(payload_fields...)`
* Constraint: `fee_payer` MUST be the sponsor's address. `fee_payer_signature` remains `0x80`.
* *Effect:* The sponsor binds their address to the specific executor intent.
* *Note:* In the final encoded transaction, `fee_payer_signature` is populated with the sponsor signature; it is set to `0x80` only for signing preimages. The "both present or both absent" rule applies to the final encoded payload.

3. **Transaction Hash** (TxHash)
* `keccak256(0x76 || rlp([payload_fields..., v, r, s]))` using the final encoded transaction (including the sponsor signature if present).

### Validity Rules

* **State:** `fee_payer` and `fee_payer_signature` MUST be both present or both absent.
* **Behavior:**
* If sponsorship is absent: Executor pays gas (standard EIP-1559 behavior).
* If sponsorship is present: Sponsor pays gas; executor remains `from` (tx.origin).

* **Validation:**
* Executor signature MUST be valid for domain `0x76`.
* If present, Sponsor signature MUST be valid for domain `0x78`.
* `calls` MUST contain at least one call.
* Only the **first** call MAY be a `CREATE` call; all subsequent calls MUST be `CALL`.

* **Trusted Ingestion (L2/DA):**
* Transactions derived from trusted sources (e.g., L1 Data Availability) bypass the TxPool. These MUST undergo full signature validation (Executor + Sponsor) within the payload builder or execution pipeline before processing to ensure integrity.

### Batch Calls

Batch calls are executed **atomically**: either all calls succeed or the entire transaction reverts. There are no partial successes.

Operational constraints:
* The entire batch is signed once by the executor.
* Intrinsic gas MUST be computed over **all** calls in the batch (calldata, cold access per call, CREATE cost, and any signature-related costs).
* If any call fails, all state changes from previous calls in the batch MUST be reverted.

## Implementation Strategy

We will utilize Reth's `NodeTypes` configuration to wire these primitives without modifying core crates.

### 1. Primitives Layer (`crates/ev-primitives`)

* Define `EvTxEnvelope` enum implementing `TransactionEnvelope` and `alloy_rlp` traits.
* Implement custom signing and recovery logic (`recover_executor`, `recover_sponsor`).
* Ensure the executor signature is carried by the envelope as `Signed<EvNodeTransaction>` and encoded as `v,r,s` (not inside the payload).

```rust
#[derive(Clone, Debug, alloy_consensus::TransactionEnvelope)]
#[envelope(ty = 0x76)]
pub enum EvTxEnvelope {
// ... Standard variants (0, 1, 2, 3)
EvNode(EvNodeSignedTx),
}
```

### 2. Node Configuration (`crates/node`)

* **Ingress (Attributes):** Update `attributes.rs` to decode `0x76` payloads using `EvTxEnvelope`.
* **Persistence:** Configure the node's storage generic to use `EthStorage<EvTxEnvelope>`. Ensure database codecs (`Compact` implementation) handle the `0x76` variant efficiently.
* **Validation (TxPool):** Implement a custom `TransactionValidator`.
* The validator MUST verify the sponsor signature (if present) before admitting the tx to the pool to prevent DoS attacks.
* Check sponsor balance against `gas_limit * max_fee`.

### 3. Execution Layer (`crates/ev-revm`)

* **Handler:** Extend `ConfigureEvm` or implement a custom `EvmHandler`.
* **Fee Deduction:** Override the standard fee deduction logic.
* If `tx.type == 0x76` and `fee_payer` is present, debit the `fee_payer` account in the REVM database.
* Otherwise, fallback to standard deduction (debit `caller`).
* **Batch Execution:** Execute `calls` sequentially under an outer checkpoint; revert all state if any call fails.
* **Context:** Map `EvNodeTransaction` to `TxEnv`. Ensure `TxEnv.caller` is always the executor.

### 4. RPC & Observability

Standard Ethereum JSON-RPC types do not support sponsorship fields. We must extend the RPC layer (e.g., via `EthApiBuilder`):

* **Transactions:** `eth_getTransactionByHash` response MUST include `feePayer` (address) if present.
* **Receipts:** `eth_getTransactionReceipt` MUST indicate the effective gas payer for indexing purposes.

## Security Considerations

### Sponsor Malleability (Front-running)

Since the executor signs an empty sponsor field (`0x80`), a valid signed transaction is "sponsor-agnostic".

* **Risk:** A malicious actor could observe a pending sponsored transaction, replace the `fee_payer` address with their own, re-sign the sponsor part, and submit it.
* **Impact:** Low. If the malicious actor pays the gas, the executor's intent is still fulfilled. This enables "Open Gas Station" networks where any relayer can pick up transactions.

### Denial of Service (DoS)

Signature recovery is expensive (`ecrecover`).

* **Risk:** An attacker floods the node with valid executor signatures but invalid sponsor signatures.
* **Mitigation:** The `TransactionValidator` in the P2P/RPC ingress layer must strictly validate both signatures before propagation or pooling.

## References

* [EIP-2718: Typed Transaction Envelope](https://eips.ethereum.org/EIPS/eip-2718)
* [Reth Custom Node Example](https://github.com/paradigmxyz/reth/tree/main/examples/custom-node)
* [Tempo Protocol Specifications](https://github.com/tempoxyz/tempo)