Skip to content

Commit dae3ab6

Browse files
committed
add e2e test for signature of sponsorship
1 parent 185a52e commit dae3ab6

3 files changed

Lines changed: 220 additions & 5 deletions

File tree

crates/ev-primitives/src/tx.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ impl EvNodeTransaction {
135135
}
136136

137137
fn encoded_payload_with_executor(&self, executor: Address) -> Vec<u8> {
138-
let mut out =
139-
Vec::with_capacity(self.payload_fields_length(self.fee_payer_signature.as_ref()) + 32);
138+
// Sponsor signatures must be computed over the unsigned sponsor field to avoid
139+
// self-referential hashing.
140+
let mut out = Vec::with_capacity(self.payload_fields_length(None) + 32);
140141
out.extend_from_slice(executor.as_slice());
141-
self.encode_payload_fields(&mut out, self.fee_payer_signature.as_ref());
142+
self.encode_payload_fields(&mut out, None);
142143
out
143144
}
144145

crates/node/src/rpc.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ impl EvRpcTransaction {
5858
const fn new(inner: Transaction<EvTxEnvelope>, fee_payer: Option<Address>) -> Self {
5959
Self { inner, fee_payer }
6060
}
61+
62+
/// Returns the optional fee payer address.
63+
pub const fn fee_payer(&self) -> Option<Address> {
64+
self.fee_payer
65+
}
66+
67+
/// Returns the inner transaction.
68+
pub const fn inner(&self) -> &Transaction<EvTxEnvelope> {
69+
&self.inner
70+
}
6171
}
6272

6373
impl ConsensusTransaction for EvRpcTransaction {
@@ -174,6 +184,16 @@ impl EvRpcReceipt {
174184
) -> Self {
175185
Self { inner, fee_payer }
176186
}
187+
188+
/// Returns the optional fee payer address.
189+
pub const fn fee_payer(&self) -> Option<Address> {
190+
self.fee_payer
191+
}
192+
193+
/// Returns the inner receipt.
194+
pub const fn inner(&self) -> &TransactionReceipt<AnyReceiptEnvelope<Log>> {
195+
&self.inner
196+
}
177197
}
178198

179199
impl ReceiptResponse for EvRpcReceipt {

crates/tests/src/e2e_tests.rs

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use alloy_consensus::{TxEnvelope, TxReceipt};
2-
use alloy_eips::{eip2718::Encodable2718, BlockNumberOrTag};
1+
use alloy_consensus::{SignableTransaction, TxEnvelope, TxReceipt};
2+
use alloy_eips::{eip2718::Encodable2718, eip2930::AccessList, BlockNumberOrTag};
33
use alloy_network::eip2718::Decodable2718;
44
use alloy_primitives::{address, Address, Bytes, TxKind, B256, U256};
55
use alloy_rpc_types::{
@@ -10,6 +10,7 @@ use alloy_rpc_types::{
1010
BlockId,
1111
};
1212
use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes, PayloadStatusEnum};
13+
use alloy_signer::SignerSync;
1314
use alloy_sol_types::{sol, SolCall};
1415
use eyre::Result;
1516
use futures::future;
@@ -28,6 +29,8 @@ use crate::common::{
2829
create_test_chain_spec, create_test_chain_spec_with_base_fee_sink,
2930
create_test_chain_spec_with_mint_admin, TEST_CHAIN_ID,
3031
};
32+
use ev_node::rpc::{EvRpcReceipt, EvRpcTransaction, EvTransactionRequest};
33+
use ev_primitives::{Call, EvNodeTransaction, EvTxEnvelope};
3134
use ev_precompiles::mint::MINT_PRECOMPILE_ADDR;
3235

3336
sol! {
@@ -374,6 +377,197 @@ async fn test_e2e_base_fee_sink_receives_base_fee() -> Result<()> {
374377
Ok(())
375378
}
376379

380+
/// Tests that a sponsored EvNode transaction charges gas to the sponsor, not the executor.
381+
///
382+
/// # Test Flow
383+
/// 1. Creates an executor and sponsor account from genesis-funded wallets
384+
/// 2. Builds a sponsored EvNode transfer transaction (type 0x76)
385+
/// 3. Includes the transaction in a block via the Engine API
386+
/// 4. Verifies `feePayer` appears in the RPC tx and receipt responses
387+
/// 5. Asserts balances: executor pays value, sponsor pays gas
388+
///
389+
/// # Success Criteria
390+
/// - Receipt and transaction expose the sponsor address
391+
/// - Recipient receives the transfer value
392+
/// - Executor balance decreases by exactly `value`
393+
/// - Sponsor balance decreases by exactly `gas_used * effective_gas_price`
394+
#[tokio::test(flavor = "multi_thread")]
395+
async fn test_e2e_sponsored_evnode_transaction() -> Result<()> {
396+
reth_tracing::init_test_tracing();
397+
398+
let chain_spec = create_test_chain_spec();
399+
let chain_id = chain_spec.chain().id();
400+
401+
let mut setup = Setup::<EvolveEngineTypes>::default()
402+
.with_chain_spec(chain_spec)
403+
.with_network(NetworkSetup::single_node())
404+
.with_dev_mode(true);
405+
406+
let mut env = Environment::<EvolveEngineTypes>::default();
407+
setup.apply::<EvolveNode>(&mut env).await?;
408+
409+
let parent_block = env.node_clients[0]
410+
.get_block_by_number(BlockNumberOrTag::Latest)
411+
.await?
412+
.expect("parent block should exist");
413+
let mut parent_hash = parent_block.header.hash;
414+
let mut parent_timestamp = parent_block.header.inner.timestamp;
415+
let mut parent_number = parent_block.header.inner.number;
416+
let gas_limit = parent_block.header.inner.gas_limit;
417+
418+
let mut wallets = Wallet::new(3).with_chain_id(chain_id).wallet_gen();
419+
let executor = wallets.remove(0);
420+
let sponsor = wallets.remove(0);
421+
let recipient = Address::random();
422+
let executor_address = executor.address();
423+
let sponsor_address = sponsor.address();
424+
425+
let executor_balance_before =
426+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
427+
&env.node_clients[0].rpc,
428+
executor_address,
429+
Some(BlockId::latest()),
430+
)
431+
.await?;
432+
let sponsor_balance_before =
433+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
434+
&env.node_clients[0].rpc,
435+
sponsor_address,
436+
Some(BlockId::latest()),
437+
)
438+
.await?;
439+
let recipient_balance_before =
440+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
441+
&env.node_clients[0].rpc,
442+
recipient,
443+
Some(BlockId::latest()),
444+
)
445+
.await?;
446+
447+
let executor_nonce = EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::transaction_count(
448+
&env.node_clients[0].rpc,
449+
executor_address,
450+
Some(BlockId::latest()),
451+
)
452+
.await?;
453+
let executor_nonce = u64::try_from(executor_nonce).expect("nonce fits into u64");
454+
455+
let transfer_value = U256::from(1_000_000_000_000_000u64); // 0.001 ETH
456+
let call = Call {
457+
to: TxKind::Call(recipient),
458+
value: transfer_value,
459+
input: Bytes::default(),
460+
};
461+
462+
let ev_tx = EvNodeTransaction {
463+
chain_id,
464+
nonce: executor_nonce,
465+
max_priority_fee_per_gas: 1_000_000_000,
466+
max_fee_per_gas: 2_000_000_000,
467+
gas_limit: 100_000,
468+
calls: vec![call],
469+
access_list: AccessList::default(),
470+
fee_payer_signature: None,
471+
};
472+
473+
let executor_sig = executor
474+
.sign_hash_sync(&ev_tx.signature_hash())
475+
.expect("executor signature");
476+
let mut signed = ev_tx.into_signed(executor_sig);
477+
let sponsor_hash = signed.tx().sponsor_signing_hash(executor_address);
478+
let sponsor_sig = sponsor
479+
.sign_hash_sync(&sponsor_hash)
480+
.expect("sponsor signature");
481+
signed.tx_mut().fee_payer_signature = Some(sponsor_sig);
482+
483+
let envelope = EvTxEnvelope::EvNode(signed);
484+
let raw_tx: Bytes = envelope.encoded_2718().into();
485+
let tx_hash = *envelope.tx_hash();
486+
487+
build_block_with_transactions(
488+
&mut env,
489+
&mut parent_hash,
490+
&mut parent_number,
491+
&mut parent_timestamp,
492+
Some(gas_limit),
493+
vec![raw_tx],
494+
Address::ZERO,
495+
)
496+
.await?;
497+
498+
type EvRpcBlock = Block<EvRpcTransaction, Header>;
499+
let receipt = EthApiClient::<EvTransactionRequest, EvRpcTransaction, EvRpcBlock, EvRpcReceipt, Header>::transaction_receipt(
500+
&env.node_clients[0].rpc,
501+
tx_hash,
502+
)
503+
.await?
504+
.expect("sponsored transaction receipt available");
505+
let receipt_inner = receipt.inner();
506+
assert!(receipt_inner.status(), "sponsored transaction should succeed");
507+
assert_eq!(
508+
receipt.fee_payer(),
509+
Some(sponsor_address),
510+
"receipt should expose sponsor fee payer"
511+
);
512+
513+
let tx = EthApiClient::<EvTransactionRequest, EvRpcTransaction, EvRpcBlock, EvRpcReceipt, Header>::transaction_by_hash(
514+
&env.node_clients[0].rpc,
515+
tx_hash,
516+
)
517+
.await?
518+
.expect("sponsored transaction available");
519+
assert_eq!(
520+
tx.fee_payer(),
521+
Some(sponsor_address),
522+
"transaction should expose sponsor fee payer"
523+
);
524+
525+
let executor_balance_after =
526+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
527+
&env.node_clients[0].rpc,
528+
executor_address,
529+
Some(BlockId::latest()),
530+
)
531+
.await?;
532+
let sponsor_balance_after =
533+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
534+
&env.node_clients[0].rpc,
535+
sponsor_address,
536+
Some(BlockId::latest()),
537+
)
538+
.await?;
539+
let recipient_balance_after =
540+
EthApiClient::<TransactionRequest, Transaction, Block, Receipt, Header>::balance(
541+
&env.node_clients[0].rpc,
542+
recipient,
543+
Some(BlockId::latest()),
544+
)
545+
.await?;
546+
547+
let executor_spent = executor_balance_before.saturating_sub(executor_balance_after);
548+
let sponsor_spent = sponsor_balance_before.saturating_sub(sponsor_balance_after);
549+
let recipient_gain = recipient_balance_after.saturating_sub(recipient_balance_before);
550+
assert_eq!(
551+
recipient_gain, transfer_value,
552+
"recipient should receive transfer value"
553+
);
554+
assert_eq!(
555+
executor_spent, transfer_value,
556+
"executor should only pay value when sponsored"
557+
);
558+
559+
let expected_gas_cost = U256::from(receipt_inner.gas_used)
560+
.saturating_mul(U256::from(receipt_inner.effective_gas_price));
561+
assert_eq!(
562+
sponsor_spent, expected_gas_cost,
563+
"sponsor should pay gas cost"
564+
);
565+
566+
drop(setup);
567+
568+
Ok(())
569+
}
570+
377571
/// Tests minting and burning tokens to/from a dynamically generated wallet not in genesis.
378572
///
379573
/// # Test Flow

0 commit comments

Comments
 (0)