|
| 1 | +# ADR 0003: Typed Transactions for Sponsorship and Batch Calls (Type 0x76) |
| 2 | + |
| 3 | +## Changelog |
| 4 | +* 2026-01-05: Initial draft structure. |
| 5 | + |
| 6 | +## Status |
| 7 | +DRAFT — Not Implemented |
| 8 | + |
| 9 | +## Abstract |
| 10 | + |
| 11 | +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 sponsor (gas provider, recovered from the sponsor signature). 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. |
| 12 | + |
| 13 | +## Context |
| 14 | + |
| 15 | +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: |
| 16 | +1. **Smart Contract Wallets (ERC-4337):** High gas overhead and complexity. |
| 17 | +2. **Meta-transactions (EIP-712):** Requires specific contract support on the destination. |
| 18 | + |
| 19 | +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. |
| 20 | + |
| 21 | +Terminology: the **executor** is the signer of domain `0x76`; it provides the `nonce`, is the transaction `from`, and maps to `tx.origin`. The **sponsor** is the signer of domain `0x78` and pays gas when sponsorship is present. **Sponsorship** means `fee_payer_signature` is present and pays gas; it does not change the `from`. |
| 22 | + |
| 23 | +## Decision |
| 24 | + |
| 25 | +We will implement a custom EIP-2718 transaction type `0x76` (`EvNodeTransaction`) that encodes **batched calls** plus an optional sponsor authorization. |
| 26 | + |
| 27 | +**Key Architectural Decisions:** |
| 28 | + |
| 29 | +1. **Dual Signature Scheme:** The transaction supports two signature domains. The Executor signature authorizes the action; the Sponsor signature authorizes the gas payment. |
| 30 | +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. |
| 31 | +3. **Batch Calls are Atomic:** All calls succeed or the entire transaction reverts; there are no partial successes. |
| 32 | +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. |
| 33 | +5. **Persistence:** 0x76 transactions are persisted as part of block bodies using a custom envelope in `EthStorage`. |
| 34 | + |
| 35 | +## Specification |
| 36 | + |
| 37 | +### Transaction Structure |
| 38 | + |
| 39 | +**Type Byte:** `0x76` |
| 40 | + |
| 41 | +The **payload** contains the following fields, RLP encoded. Field order is consensus-critical: |
| 42 | + |
| 43 | +```rust |
| 44 | +pub struct EvNodeTransaction { |
| 45 | + // EIP-1559-like fields |
| 46 | + pub chain_id: u64, |
| 47 | + pub nonce: u64, |
| 48 | + pub max_priority_fee_per_gas: u128, |
| 49 | + pub max_fee_per_gas: u128, |
| 50 | + pub gas_limit: u64, |
| 51 | + pub calls: Vec<Call>, |
| 52 | + pub access_list: AccessList, |
| 53 | + // Sponsorship Extensions (Optional) |
| 54 | + pub fee_payer_signature: Option<Signature>, |
| 55 | +} |
| 56 | + |
| 57 | +pub struct Call { |
| 58 | + pub to: TxKind, |
| 59 | + pub value: U256, |
| 60 | + pub input: Bytes, |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +The **signed envelope** is a standard typed-transaction envelope with the executor signature: |
| 65 | + |
| 66 | +```rust |
| 67 | +pub type EvNodeSignedTx = Signed<EvNodeTransaction>; |
| 68 | +``` |
| 69 | + |
| 70 | +### Encoding (RLP) |
| 71 | + |
| 72 | +Optional fields MUST be encoded deterministically: |
| 73 | + |
| 74 | +* `fee_payer_signature`: encode `0x80` (nil) when `None`. |
| 75 | + |
| 76 | +The `calls` field is an RLP list of `Call` structs, each encoded as: |
| 77 | + |
| 78 | +``` |
| 79 | +rlp([to, value, input]) |
| 80 | +``` |
| 81 | + |
| 82 | +**Signed envelope encoding (executor signature):** |
| 83 | +* The final encoded transaction is `0x76 || rlp([payload_fields..., v, r, s])`, matching EIP-1559-style typed tx encoding. |
| 84 | +* The `payload_fields...` are exactly the fields in `EvNodeTransaction` above, in order. |
| 85 | + |
| 86 | +### Signatures and Hashing |
| 87 | + |
| 88 | +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. |
| 89 | + |
| 90 | +1. **Executor Signature** (Domain `0x76`) |
| 91 | +* Preimage: `0x76 || rlp(payload_fields...)` (no `v,r,s` in the RLP). |
| 92 | +* Constraint: `fee_payer_signature` MUST be set to `0x80` (empty) in the RLP stream for this hash. |
| 93 | +* *Effect:* The executor authorizes the intent regardless of who pays. |
| 94 | + |
| 95 | +2. **Sponsor Signature** (Domain `0x78`) |
| 96 | +* Preimage: `0x78 || rlp(payload_fields...)` with `fee_payer_signature` set to `0x80`, and the executor `sender` address encoded in its place for the hash. |
| 97 | +* *Effect:* The sponsor binds to a specific executor intent and can be recovered from the signature. |
| 98 | +* *Note:* In the final encoded transaction, `fee_payer_signature` is populated with the sponsor signature; it is set to `0x80` only for signing preimages. |
| 99 | + |
| 100 | +3. **Transaction Hash** (TxHash) |
| 101 | +* `keccak256(0x76 || rlp([payload_fields..., v, r, s]))` using the final encoded transaction (including the sponsor signature if present). |
| 102 | + |
| 103 | +### Validity Rules |
| 104 | + |
| 105 | +* **State:** `fee_payer_signature` is optional; if absent, the transaction is not sponsored. |
| 106 | +* **Behavior:** |
| 107 | + * If sponsorship is absent: Executor pays gas (standard EIP-1559 behavior). |
| 108 | + * If sponsorship is present: Sponsor pays gas (sponsor recovered from signature); executor remains `from` (tx.origin). |
| 109 | + |
| 110 | +* **Validation:** |
| 111 | + * Executor signature MUST be valid for domain `0x76`. |
| 112 | + * If present, Sponsor signature MUST be valid for domain `0x78`. |
| 113 | + * `calls` MUST contain at least one call. |
| 114 | + * Only the **first** call MAY be a `CREATE` call; all subsequent calls MUST be `CALL`. |
| 115 | + |
| 116 | +* **Trusted Ingestion (L2/DA):** |
| 117 | + * 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. |
| 118 | + |
| 119 | +### Batch Calls |
| 120 | + |
| 121 | +Batch calls are executed **atomically**: either all calls succeed or the entire transaction reverts. There are no partial successes. |
| 122 | + |
| 123 | +Operational constraints: |
| 124 | +* The entire batch is signed once by the executor. |
| 125 | +* Intrinsic gas MUST be computed over **all** calls in the batch (calldata, cold access per call, CREATE cost, and any signature-related costs). |
| 126 | +* If any call fails, all state changes from previous calls in the batch MUST be reverted. |
| 127 | + |
| 128 | +## Sponsorship Flow (Genesis → Sponsor Signature) |
| 129 | + |
| 130 | +This section describes an end-to-end flow for creating a sponsored `0x76` transaction, from initial intent to sponsor signing and submission. It complements (but does not replace) the rules in “Signatures and Hashing”. |
| 131 | + |
| 132 | +### 0) Pre-conditions / Genesis State |
| 133 | +- The executor has a key pair, nonce space, and required permissions for the calls. |
| 134 | +- The sponsor has funds and is willing to pay gas for the executor’s intent. |
| 135 | +- The protocol does **not** implement an automated fee-paying system; sponsorship is arranged off-chain. |
| 136 | +- A sponsorship service **may** be used to provide fee sponsorship, but for now this is the responsibility of the chain. |
| 137 | + |
| 138 | +### 1) Executor Builds the Unsponsored Payload (Intent) |
| 139 | +- The executor constructs `EvNodeTransaction` with: |
| 140 | + - `fee_payer_signature = None` |
| 141 | + - all call data, gas params, and access list |
| 142 | +- The executor signs the **executor signature hash**: |
| 143 | + - `hash_exec = keccak256(0x76 || rlp(payload_fields... with fee_payer_signature = 0x80))` |
| 144 | +- The executor produces `executor_signature` (secp256k1), forming `Signed<EvNodeTransaction>`. |
| 145 | + |
| 146 | +### 2) Sponsor-Ready Envelope (Unsigned by Sponsor) |
| 147 | +- The executor shares the payload + executor signature with a sponsor (directly or via a service). |
| 148 | +- The payload is unchanged; `fee_payer_signature` remains empty. |
| 149 | +- **Broadcast readiness:** at this point the tx is valid but **unsponsored** and can be broadcast as a normal executor-paid transaction. |
| 150 | + |
| 151 | +### 3) Sponsor Computes the Sponsor Hash |
| 152 | +- The sponsor computes: |
| 153 | + - `hash_sponsor = keccak256(0x78 || rlp(payload_fields... with fee_payer_signature = 0x80))` |
| 154 | + - The sponsor uses the same payload as the executor (no mutation other than the domain byte). |
| 155 | + |
| 156 | +### 4) Sponsor Signs and Fills `fee_payer_signature` |
| 157 | +- The sponsor signs `hash_sponsor` **off-chain** (e.g., within the app or via an app-side signing service) and obtains `fee_payer_signature`. |
| 158 | +- The transaction payload is updated: |
| 159 | + - `fee_payer_signature = Some(sponsor_signature)` |
| 160 | +- The sponsor can verify that `recover_fee_payer(hash_sponsor, signature)` returns their address. |
| 161 | +- **Broadcast readiness:** once `fee_payer_signature` is present, the tx is fully sponsored and can be broadcast. |
| 162 | + |
| 163 | +### 5) Submission and Validation |
| 164 | +- The fully formed typed tx is: |
| 165 | + - `0x76 || rlp([payload_fields..., v, r, s])` with `fee_payer_signature` included in the payload |
| 166 | +- Validation path: |
| 167 | + - Executor signature verified on domain `0x76` |
| 168 | + - Sponsor signature verified on domain `0x78` |
| 169 | + - Sponsor address recovered from signature and used for fee checks / balance |
| 170 | + |
| 171 | +### 6) Execution and Receipt |
| 172 | +- Execution occurs with `tx.origin` = executor. |
| 173 | +- Gas is charged to the recovered sponsor address. |
| 174 | + |
| 175 | +## Implementation Strategy |
| 176 | + |
| 177 | +We will utilize Reth's `NodeTypes` configuration to wire these primitives without modifying core crates. |
| 178 | + |
| 179 | +### 1. Primitives Layer (`crates/ev-primitives`) |
| 180 | + |
| 181 | +* Define `EvTxEnvelope` enum implementing `TransactionEnvelope` and `alloy_rlp` traits. |
| 182 | +* Implement custom signing and recovery logic (`recover_executor`, `recover_sponsor`). |
| 183 | +* Ensure the executor signature is carried by the envelope as `Signed<EvNodeTransaction>` and encoded as `v,r,s` (not inside the payload). |
| 184 | + |
| 185 | +```rust |
| 186 | +#[derive(Clone, Debug, alloy_consensus::TransactionEnvelope)] |
| 187 | +#[envelope(ty = 0x76)] |
| 188 | +pub enum EvTxEnvelope { |
| 189 | + // ... Standard variants (0, 1, 2, 3) |
| 190 | + EvNode(EvNodeSignedTx), |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +### 2. Node Configuration (`crates/node`) |
| 195 | + |
| 196 | +* **Ingress (Attributes):** Update `attributes.rs` to decode `0x76` payloads using `EvTxEnvelope`. |
| 197 | +* **Persistence:** Configure the node's storage generic to use `EthStorage<EvTxEnvelope>`. Ensure database codecs (`Compact` implementation) handle the `0x76` variant efficiently. |
| 198 | +* **Validation (TxPool):** Implement a custom `TransactionValidator`. |
| 199 | + * The validator MUST verify the sponsor signature (if present) before admitting the tx to the pool to prevent DoS attacks. |
| 200 | + * Check sponsor balance against `gas_limit * max_fee`. |
| 201 | + |
| 202 | +### 3. Execution Layer (`crates/ev-revm`) |
| 203 | + |
| 204 | +* **Handler:** Extend `ConfigureEvm` or implement a custom `EvmHandler`. |
| 205 | +* **Fee Deduction:** Override the standard fee deduction logic. |
| 206 | + * If `tx.type == 0x76` and `fee_payer_signature` is present, debit the **recovered sponsor** account in the REVM database. |
| 207 | + * Otherwise, fallback to standard deduction (debit `caller`). |
| 208 | +* **Batch Execution:** Execute `calls` sequentially under an outer checkpoint; revert all state if any call fails. |
| 209 | +* **Context:** Map `EvNodeTransaction` to `TxEnv`. Ensure `TxEnv.caller` is always the executor. |
| 210 | + |
| 211 | +### 4. RPC & Observability |
| 212 | + |
| 213 | +Standard Ethereum JSON-RPC types do not support sponsorship fields. We must extend the RPC layer (e.g., via `EthApiBuilder`): |
| 214 | + |
| 215 | +* **Transactions:** `eth_getTransactionByHash` response MUST include `feePayer` (address) if present. |
| 216 | +* **Receipts:** `eth_getTransactionReceipt` MUST indicate the effective gas payer for indexing purposes. |
| 217 | + |
| 218 | +## Client Integration (Viem) |
| 219 | + |
| 220 | +We will use **Viem** and create a **custom client** based on the Viem custom client pattern (see: `https://viem.sh/docs/clients/custom`). This client will encapsulate `0x76` transaction creation and sponsorship signing. |
| 221 | + |
| 222 | +## Security Considerations |
| 223 | + |
| 224 | +### Sponsor Malleability (Front-running) |
| 225 | + |
| 226 | +Since the executor signs an empty sponsor field (`0x80`), a valid signed transaction is "sponsor-agnostic". |
| 227 | + |
| 228 | +* **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. |
| 229 | +* **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. |
| 230 | + |
| 231 | +### Denial of Service (DoS) |
| 232 | + |
| 233 | +Signature recovery is expensive (`ecrecover`). |
| 234 | + |
| 235 | +* **Risk:** An attacker floods the node with valid executor signatures but invalid sponsor signatures. |
| 236 | +* **Mitigation:** The `TransactionValidator` in the P2P/RPC ingress layer must strictly validate both signatures before propagation or pooling. |
| 237 | + |
| 238 | +## References |
| 239 | + |
| 240 | +* [EIP-2718: Typed Transaction Envelope](https://eips.ethereum.org/EIPS/eip-2718) |
| 241 | +* [Reth Custom Node Example](https://github.com/paradigmxyz/reth/tree/main/examples/custom-node) |
| 242 | +* [Tempo Protocol Specifications](https://github.com/tempoxyz/tempo) |
0 commit comments