@@ -14,7 +14,9 @@ This ADR proposes an additional EvNode transaction type that includes gas
1414sponsorship as a first-class capability, using EIP-2718 typed transactions.
1515The idea is to define a typed transaction format that separates the gas payer
1616from the executor so the cost can be covered without altering the normal
17- execution flow. This reduces complexity for users and integrations.
17+ execution flow. This reduces complexity for users and integrations. The design
18+ defines custom primitives and wrappers locally in this repo and then injects
19+ them into node components, without modifying reth.
1820
1921## Context
2022
@@ -38,7 +40,8 @@ reth's transaction validation and propagation layers.
3840This ADR assumes EvNode does not use the transaction pool: 0x76 transactions
3941are accepted only via Engine API/payload building paths. As a result, there is
4042no pool-level validation for this type; validation occurs during decode and
41- execution.
43+ execution. The pool and ` eth_sendRawTransaction ` paths are explicitly out of
44+ scope for this ADR.
4245
4346## Decision
4447
@@ -53,12 +56,16 @@ separate executor and sponsor signature domains, so it requires a custom signed
5356wrapper and signature hashing logic.
5457The executor is the canonical sender (` from ` ) and owns the nonce; EVM execution
5558semantics (CALLER) are always based on the executor. The sponsor only pays fees.
59+ Implementation will define local transaction primitives and envelopes in this
60+ repo and inject them into node builders, without modifying reth crates.
5661
5762## Implementation Plan
5863
59- 1 . Define the consensus transaction envelope and type.
60- - Define the ` EvNodeTransaction ` struct and ` EvRethTxEnvelope ` enum in
61- ` crates/primitives ` , using a custom signed wrapper.
64+ 1 . Define local primitives and transaction envelope.
65+ - Add a new local crate (e.g. ` crates/ev-primitives ` ) to host the transaction
66+ types and wrappers.
67+ - Define the ` EvNodeTransaction ` struct, ` EvNodeSignedTx ` wrapper, and
68+ ` EvTxEnvelope ` enum in that crate, using a custom signed wrapper.
6269 - Register the new typed transaction with ` #[envelope(ty = 0x76)] ` and keep
6370 the consensus field ordering explicit in the struct.
6471
@@ -72,7 +79,7 @@ semantics (CALLER) are always based on the executor. The sponsor only pays fees.
7279)]
7380#[cfg_attr(test, reth_codecs:: add_arbitrary_tests(compact, rlp))]
7481#[expect(clippy:: large_enum_variant)]
75- pub enum EvRethTxEnvelope {
82+ pub enum EvTxEnvelope {
7683 #[envelope(ty = 0)]
7784 Legacy (Signed <TxLegacy >),
7885 #[envelope(ty = 1)]
@@ -81,7 +88,7 @@ pub enum EvRethTxEnvelope {
8188 Eip1559 (Signed <TxEip1559 >),
8289 #[envelope(ty = 3)]
8390 Eip4844 (Signed <TxEip4844 >),
84- #[envelope(ty = 0x76]
91+ #[envelope(ty = 0x76) ]
8592 EvNode (EvNodeSignedTx ),
8693}
8794
@@ -101,37 +108,39 @@ pub struct EvNodeTransaction {
101108 // These mirror EIP-1559 fields to stay compatible with the standard.
102109 pub chain_id : u64 ,
103110 pub nonce : u64 ,
104- pub gas_limit : u64 ,
105- pub max_fee_per_gas : u128 ,
106111 pub max_priority_fee_per_gas : u128 ,
112+ pub max_fee_per_gas : u128 ,
113+ pub gas_limit : u64 ,
107114 pub to : TxKind ,
108115 pub value : U256 ,
109116 pub data : Bytes ,
110117 pub access_list : AccessList ,
111118 // Sponsorship fields (payer is separate, optional capability)
119+ pub fee_payer : Option <Address >,
112120 pub fee_payer_signature : Option <Signature >,
113121}
114122```
115123
1161242 . Specify encoding + signing preimages (keep deterministic signing).
117125 - Define the exact RLP field order for ` EvNodeTransaction ` :
118126 `chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to,
119- value, data, access_list, fee_payer_signature`.
127+ value, data, access_list, fee_payer, fee_payer_signature`.
120128 This order is consensus-critical; if encoding is derived from struct field
121129 order, the struct must match this ordering exactly.
122130 - Encode optional fields deterministically:
131+ - ` fee_payer ` : always encoded; if ` None ` , encode ` 0x80 ` .
123132 - ` fee_payer_signature ` : always encoded; if ` None ` , encode ` 0x80 ` .
124133 - Executor signature preimage (domain: ` 0x76 ` ):
125- - ` 0x76 || rlp(fields...) ` with ` fee_payer_signature = 0x80`
126- regardless of whether a sponsor will sign later .
134+ - ` 0x76 || rlp(fields...) ` with ` fee_payer = 0x80` and
135+ ` fee_payer_signature = 0x80 ` regardless of whether a sponsor will sign.
127136 - Sponsor signature preimage (domain: ` 0x78 ` ):
128- - ` 0x78 || rlp(fields...) ` where ` fee_payer_signature ` is replaced by the
129- executor address .
137+ - ` 0x78 || rlp(fields...) ` where ` fee_payer ` is set to the executor address
138+ and ` fee_payer_signature = 0x80 ` .
130139 - ` tx_hash ` uses standard EIP-2718 hashing:
131140 - ` keccak256(0x76 || rlp(fields...)) ` with the * final* ` fee_payer_signature ` .
132141 - Ensure the custom signed type exposes:
133- - ` executor_signature_hash() ` (placeholder sponsor signature )
134- - ` sponsor_signature_hash() ` (executor address in sponsor slot )
142+ - ` executor_signature_hash() ` (fee_payer fields empty )
143+ - ` sponsor_signature_hash() ` (fee_payer = executor address )
135144 - ` recover_executor() ` and ` recover_sponsor() ` as applicable
136145 - trait implementations required by Reth for pool/consensus encoding
137146 (` Encodable ` , ` Decodable ` , ` Encodable2718 ` , ` Decodable2718 ` , ` Transaction ` ,
@@ -143,54 +152,23 @@ pub struct EvNodeTransaction {
143152 - If ` fee_payer_signature ` is ` Some ` , the payer is the sponsor and the sponsor
144153 signature must be valid for the sponsor domain and bound to the executor.
145154
146- 4 . Add the tx type identifier and compact encoding.
147- - Register the new type id in the custom ` TxType ` enum and compact codec
148- (extended identifier if needed), so storage/network encoding works.
149- - Ensure ` TransactionEnvelope ` derives cover both the canonical and pooled
150- variants without conflicting type ids.
151-
152- Code-level implications:
153- - Add a ` EvNode ` /` EvRethTxType ` variant that maps to ` 0x76 ` .
154- - Implement ` Compact ` for the ` TxType ` enum so ` 0x76 ` round-trips through the
155- compact codec (use ` COMPACT_EXTENDED_IDENTIFIER_FLAG ` if required).
156- - Register ` #[envelope(ty = 0x76)] ` on both the canonical transaction
157- envelope and the pooled transaction envelope, so 2718 decoding matches
158- the compact encoding.
155+ 4 . Add the tx type identifier and envelope mapping (local).
156+ - Define a local ` EvTxType ` enum in ` crates/ev-primitives ` with a ` EvNode `
157+ variant mapped to ` 0x76 ` .
158+ - Ensure the local ` EvTxEnvelope ` ` #[envelope(ty = 0x76)] ` derives cover the
159+ canonical transaction envelope. Pool variants are out of scope.
159160
160161Example (non-normative):
161162
162163``` rust
163164pub const EVNODE_TX_TYPE_ID : u8 = 0x76 ;
164165
165- impl Compact for EvRethTxType {
166- fn to_compact <B >(& self , buf : & mut B ) -> usize
167- where
168- B : BufMut + AsMut <[u8 ]>,
169- {
170- match self {
171- Self :: EvNode => {
172- buf . put_u8 (EVNODE_TX_TYPE_ID );
173- COMPACT_EXTENDED_IDENTIFIER_FLAG
174- }
175- Self :: Op (ty ) => ty . to_compact (buf ),
176- }
177- }
178-
179- fn from_compact (mut buf : & [u8 ], identifier : usize ) -> (Self , & [u8 ]) {
180- match identifier {
181- COMPACT_EXTENDED_IDENTIFIER_FLAG => {
182- let extended_identifier = buf . get_u8 ();
183- match extended_identifier {
184- EVNODE_TX_TYPE_ID => (Self :: EvNode , buf ),
185- _ => panic! (" Unsupported TxType identifier: {extended_identifier}" ),
186- }
187- }
188- v => {
189- let (inner , buf ) = EvRethTxType :: from_compact (buf , v );
190- (inner , buf )
191- }
192- }
193- }
166+ pub enum EvTxType {
167+ Legacy ,
168+ Eip2930 ,
169+ Eip1559 ,
170+ Eip4844 ,
171+ EvNode ,
194172}
195173```
196174
@@ -235,17 +213,17 @@ match tx.tx() {
235213}
236214```
237215
238- 6 . Decode in Engine API payloads and validate.
239- - Update the payload transaction iterator to decode the custom type using
240- 2718 decoding, recover signer, and preserve the encoded bytes.
216+ 6 . Decode in Engine API payloads and validate (no pool) .
217+ - Update the Engine API transaction decoding to use ` EvTxEnvelope ` 2718
218+ decoding, recover signer, and preserve the encoded bytes.
241219 - Add fast, stateless validation for sponsorship fields during payload
242220 decoding to fail early on malformed or invalid signatures.
243221
244222Example (non-normative):
245223
246224``` rust
247225let convert = | encoded : Bytes | {
248- let tx = EvRethTxEnvelope :: decode_2718_exact (encoded . as_ref ())
226+ let tx = EvTxEnvelope :: decode_2718_exact (encoded . as_ref ())
249227 . map_err (Into :: into )
250228 . map_err (PayloadError :: Decode )? ;
251229 let signer = tx . try_recover (). map_err (NewPayloadError :: other )? ;
@@ -257,7 +235,8 @@ let convert = |encoded: Bytes| {
257235
258236Note: in this repo, the Engine API decode/validation currently happens in
259237` crates/node/src/attributes.rs ` within
260- ` PayloadBuilderAttributes::try_new ` (the ` attributes.transactions ` decoding).
238+ ` PayloadBuilderAttributes::try_new ` (the ` attributes.transactions ` decoding),
239+ and currently uses ` TransactionSigned::network_decode ` .
261240
2622417 . Define sponsorship validation and failure modes.
263242 - Specify the sponsor authorization format, signature verification, and
@@ -290,10 +269,9 @@ builder-level pre-check is optional.
290269
2912708 . RPC and receipts.
292271 - Expose an optional ` feePayer ` (or ` sponsor ` ) field for 0x76 in
293- ` eth_getTransactionByHash ` and transaction objects; ` from ` remains the
294- executor.
295- - If receipts are extended, include the same optional field for
296- observability; otherwise receipts remain standard.
272+ transaction objects for observability; ` from ` remains the executor.
273+ - If receipts are extended, include the same optional field; otherwise
274+ receipts remain standard.
297275
298276## References
299277
0 commit comments