Skip to content

Commit 5d73978

Browse files
authored
ADR 003: typed sponsorship transactions and batch execution (#96)
* Draft ADR for typed sponsorship transactions * Add SponsorTransaction type * Revert "Add SponsorTransaction type" This reverts commit a531fc6. * Update ADR for standard-signed typed tx * Expand ADR with payload signing details * Document sponsorship validation locations * Document fee payer execution hook * update * update all the spec steps * write reference for tempo * remove file * update snippet for transaction signature * use txkind to support also contracts * clarify is not the only transaction * remove some text * simplify step 1 * simplify step 2 * Remove fee token from sponsorship ADR * Document dual-domain sponsorship signatures * Clarify no pool usage for sponsorship tx * Define executor sender semantics and RPC exposure * Update ADR for local typed sponsorship tx * Align sponsorship ADR with custom primitives approach * Clarify sponsor signature semantics * docs: clarify txpoolExt_getTxs usage by ev-node * docs: add spec section for typed sponsorship tx * Clarify txpool, engine, and forced inclusion validation for 0x76 * Refine sponsorship ADR wording * Clarify persistence and remove ADR repetition * Clarify ADR layer scope * simplify spec * include batch calls * clean adr * Clarify executor signature envelope in ADR 0003 * Note executor signature envelope in implementation strategy * Clarify signature domain separators and sponsor signature encoding * Update ADR 0003 clarifications * Remove fee_payer address from ADR 0003 * Refine sponsorship flow and broadcast readiness * Add Viem custom client note
1 parent 4dfce5f commit 5d73978

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ Custom RPC namespace `txpoolExt` that provides:
5555
- Configurable byte limit for transaction retrieval (default: 1.98 MB)
5656
- Efficient iteration that stops when reaching the byte limit
5757

58+
Note: ev-node uses this endpoint indirectly. It pulls pending txs via `txpoolExt_getTxs`,
59+
then injects those RLP bytes into Engine API payload attributes (`transactions`) for block
60+
building. This means ev-reth's txpool is not used for block construction directly, but it
61+
is used as a source of transactions.
62+
5863
### 6. Base Fee Redirect
5964

6065
On vanilla Ethereum, EIP-1559 burns the base fee. For custom networks, ev-reth can redirect the base fee to a designated address:
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
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

Comments
 (0)