Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0b51b86
wip: eip7702 in pallet-revive
Wizdave97 Jan 20, 2026
bcb709b
fix tests
Wizdave97 Jan 20, 2026
0c93ae1
add signature verification tests
Wizdave97 Jan 20, 2026
6c53f25
fix refunds
Wizdave97 Jan 20, 2026
bbd96e2
nit
Wizdave97 Jan 21, 2026
a34dc06
refactor exports
Wizdave97 Jan 21, 2026
4c0365a
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 21, 2026
3f89998
nit
Wizdave97 Jan 21, 2026
a747040
Merge branch 'david/eip-7702' of github.com:polytope-labs/polkadot-sd…
Wizdave97 Jan 21, 2026
3a55134
wip: eip7702 in pallet-revive
Wizdave97 Jan 20, 2026
64e67f2
fix tests
Wizdave97 Jan 20, 2026
5fe5bc9
add signature verification tests
Wizdave97 Jan 20, 2026
0b2bb91
fix refunds
Wizdave97 Jan 20, 2026
d0115fe
nit
Wizdave97 Jan 21, 2026
c940bf4
refactor exports
Wizdave97 Jan 21, 2026
bb69263
nit
Wizdave97 Jan 21, 2026
abce9cb
Merge branch 'david/eip-7702' of github.com:polytope-labs/polkadot-sd…
Wizdave97 Jan 21, 2026
264422b
remove some unused items
Wizdave97 Jan 21, 2026
bbc05c1
add some runtime tests
Wizdave97 Jan 21, 2026
2afac32
simple refactors
Wizdave97 Jan 23, 2026
fcdbf4e
consolidate rlp encoding of authorization list entry
Wizdave97 Jan 23, 2026
dbf6960
use an enum variant to represent delegation target
Wizdave97 Jan 23, 2026
1b0d814
refactor signature verification
Wizdave97 Jan 23, 2026
0a2a94c
add benchmarks for authorization processing
Wizdave97 Jan 23, 2026
fe8621a
revert fmt changes
Wizdave97 Jan 23, 2026
0a0a7cc
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 23, 2026
18ce81a
final fmt fix
Wizdave97 Jan 23, 2026
2fc070c
Merge branch 'david/eip-7702' of github.com:polytope-labs/polkadot-sd…
Wizdave97 Jan 23, 2026
1772be2
remove remaining cosmetic changes
Wizdave97 Jan 23, 2026
934bc7a
rename some variables
Wizdave97 Jan 23, 2026
640b5e2
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 26, 2026
cad7b6f
add benchmarks
Wizdave97 Jan 26, 2026
e022d84
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 26, 2026
8df2d14
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 26, 2026
b194e4d
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 27, 2026
4fd8d79
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 28, 2026
e316f71
improved benchmarks
Wizdave97 Jan 29, 2026
8f515fb
update weights
Wizdave97 Jan 29, 2026
51ef955
Merge branch 'david/eip-7702' of github.com:polytope-labs/polkadot-sd…
Wizdave97 Jan 29, 2026
3902d2c
Merge branch 'master' into david/eip-7702
Wizdave97 Jan 29, 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
107 changes: 107 additions & 0 deletions substrate/frame/revive/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn whitelisted_pallet_account<T: Config>() -> T::AccountId {
<T as frame_system::Config>::Hash: frame_support::traits::IsType<H256>,
OriginFor<T>: From<Origin<T>>,
)]
#[benchmarks(where T: Config)]
mod benchmarks {
use super::*;

Expand All @@ -127,6 +128,111 @@ mod benchmarks {
}
}

// Benchmark for EIP-7702 single authorization validation
// Measures the weight of validating a single authorization tuple
#[benchmark(pov_mode = Measured)]
fn process_single_authorization() -> Result<(), BenchmarkError> {
use crate::evm::{
AuthorizationListEntry, SignedAuthorizationListEntry,
eip7702,
};
use k256::ecdsa::SigningKey;
use sp_core::keccak_256;

let signing_key = SigningKey::from_slice(&keccak_256(&[0u8; 32]))
.expect("valid key");

let target_address = H160::from_low_u64_be(1);

let unsigned_auth = AuthorizationListEntry {
chain_id: U256::from(T::ChainId::get()),
address: target_address,
nonce: U256::zero(),
};

let mut message = Vec::new();
message.push(eip7702::EIP7702_MAGIC);
message.extend_from_slice(&crate::evm::rlp::encode(&unsigned_auth));

let hash = keccak_256(&message);
let (signature, recovery_id) = signing_key
.sign_prehash_recoverable(&hash)
.expect("signing succeeds");

let signed_auth = SignedAuthorizationListEntry {
chain_id: unsigned_auth.chain_id,
address: unsigned_auth.address,
nonce: unsigned_auth.nonce,
y_parity: U256::from(recovery_id.to_byte()),
r: U256::from_big_endian(&signature.r().to_bytes()),
s: U256::from_big_endian(&signature.s().to_bytes()),
};

let chain_id = U256::from(T::ChainId::get());

#[block]
{
let _result = eip7702::process_single_authorization::<T>(&signed_auth, chain_id);
}

Ok(())
}

// Benchmark for EIP-7702 delegation application on existing accounts
// Measures the weight of applying delegations for `a` existing accounts
#[benchmark(pov_mode = Measured)]
fn apply_delegations_existing(a: Linear<1, 16>) -> Result<(), BenchmarkError> {
use crate::evm::eip7702;

let mut authorities = alloc::collections::BTreeMap::new();

for i in 0..a {
let authority = H160::from_low_u64_be(i as u64 + 1);
let target = H160::from_low_u64_be((i + 100) as u64);

let authority_id = T::AddressMapper::to_account_id(&authority);
frame_system::Pallet::<T>::inc_account_nonce(&authority_id);

assert!(frame_system::Account::<T>::contains_key(&authority_id),
"Account should exist after incrementing nonce");

authorities.insert(authority, target);
}

#[block]
{
let (_new_accounts, _existing_accounts) = eip7702::apply_delegations::<T>(authorities.clone());
}

Ok(())
}

// Benchmark for EIP-7702 delegation application on new accounts
// Measures the weight of applying delegations for `a` non-existing accounts
#[benchmark(pov_mode = Measured)]
fn apply_delegations_new(a: Linear<1, 16>) -> Result<(), BenchmarkError> {
use crate::evm::eip7702;

let mut authorities = alloc::collections::BTreeMap::new();

for i in 0..a {
let authority = H160::from_low_u64_be((i + 1000) as u64);
let target = H160::from_low_u64_be((i + 2000) as u64);

let authority_id = T::AddressMapper::to_account_id(&authority);
assert!(!frame_system::Account::<T>::contains_key(&authority_id));

authorities.insert(authority, target);
}

#[block]
{
let (_new_accounts, _existing_accounts) = eip7702::apply_delegations::<T>(authorities.clone());
}

Ok(())
}

#[benchmark(skip_meta, pov_mode = Measured)]
fn on_initialize_per_trie_key(k: Linear<0, 1024>) -> Result<(), BenchmarkError> {
let instance =
Expand Down Expand Up @@ -460,6 +566,7 @@ mod benchmarks {
TransactionSigned::default().signed_payload(),
effective_gas_price,
0,
vec![]
);

// contract should have received the value
Expand Down
3 changes: 3 additions & 0 deletions substrate/frame/revive/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub mod runtime;
pub mod tx_extension;
pub use alloy_core::sol_types::decode_revert_reason;

/// EIP-7702: Set EOA Account Code
pub(crate) mod eip7702;

/// Ethereum block hash builder related types.
pub(crate) mod block_hash;
pub use block_hash::ReceiptGasInfo;
Expand Down
1 change: 1 addition & 0 deletions substrate/frame/revive/src/evm/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ mod account;
pub use account::*;

mod signature;
pub use signature::recover_eth_address_from_message;
23 changes: 21 additions & 2 deletions substrate/frame/revive/src/evm/api/rlp_codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,25 @@ impl Decodable for AccessListEntry {
}

impl Encodable for AuthorizationListEntry {
fn rlp_append(&self, s: &mut rlp::RlpStream) {
s.begin_list(3);
s.append(&self.chain_id);
s.append(&self.address);
s.append(&self.nonce);
}
}

impl Decodable for AuthorizationListEntry {
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
Ok(AuthorizationListEntry {
chain_id: rlp.val_at(0)?,
address: rlp.val_at(1)?,
nonce: rlp.val_at(2)?,
})
}
}

impl Encodable for SignedAuthorizationListEntry {
fn rlp_append(&self, s: &mut rlp::RlpStream) {
s.begin_list(6);
s.append(&self.chain_id);
Expand All @@ -227,9 +246,9 @@ impl Encodable for AuthorizationListEntry {
}
}

impl Decodable for AuthorizationListEntry {
impl Decodable for SignedAuthorizationListEntry {
fn decode(rlp: &rlp::Rlp) -> Result<Self, rlp::DecoderError> {
Ok(AuthorizationListEntry {
Ok(SignedAuthorizationListEntry {
chain_id: rlp.val_at(0)?,
address: rlp.val_at(1)?,
nonce: rlp.val_at(2)?,
Expand Down
2 changes: 1 addition & 1 deletion substrate/frame/revive/src/evm/api/rpc_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ fn from_unsigned_works_for_7702() {
gas_price: U256::from(20),
max_fee_per_gas: U256::from(20),
max_priority_fee_per_gas: U256::from(1),
authorization_list: vec![AuthorizationListEntry {
authorization_list: vec![SignedAuthorizationListEntry {
chain_id: U256::from(1),
address: H160::from_low_u64_be(42),
nonce: U256::from(0),
Expand Down
48 changes: 45 additions & 3 deletions substrate/frame/revive/src/evm/api/rpc_types_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ pub struct GenericTransaction {
/// authorizationList
/// List of account code authorizations (EIP-7702)
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authorization_list: Vec<AuthorizationListEntry>,
pub authorization_list: Vec<SignedAuthorizationListEntry>,
/// blobVersionedHashes
/// List of versioned blob hashes associated with the transaction's EIP-4844 data blobs.
#[serde(default)]
Expand Down Expand Up @@ -787,7 +787,7 @@ pub struct Transaction7702Unsigned {
pub access_list: AccessList,
/// authorizationList
/// List of account code authorizations
pub authorization_list: Vec<AuthorizationListEntry>,
pub authorization_list: Vec<SignedAuthorizationListEntry>,
/// chainId
/// Chain ID that this transaction is valid on.
pub chain_id: U256,
Expand Down Expand Up @@ -821,7 +821,8 @@ pub struct Transaction7702Unsigned {
pub value: U256,
}

/// Authorization list entry for EIP-7702
/// Authorization list entry for EIP-7702 (unsigned)
/// Contains the authorization tuple without signature components
#[derive(
Debug,
Default,
Expand All @@ -843,6 +844,31 @@ pub struct AuthorizationListEntry {
pub address: Address,
/// Nonce of the authorization
pub nonce: U256,
}

/// Signed authorization list entry for EIP-7702
/// Contains the authorization tuple with signature components
#[derive(
Debug,
Default,
Clone,
Serialize,
Deserialize,
Eq,
PartialEq,
TypeInfo,
Encode,
Decode,
DecodeWithMemTracking,
)]
#[serde(rename_all = "camelCase")]
pub struct SignedAuthorizationListEntry {
/// Chain ID that this authorization is valid on
pub chain_id: U256,
/// Address to authorize
pub address: Address,
/// Nonce of the authorization
pub nonce: U256,
/// y-parity of the signature
pub y_parity: U256,
/// r component of signature
Expand All @@ -851,6 +877,22 @@ pub struct AuthorizationListEntry {
pub s: U256,
}

impl SignedAuthorizationListEntry {
/// Convert signature components (r, s, y_parity) into a 65-byte ECDSA signature.
///
/// # Returns
/// A 65-byte array containing: [r (32 bytes), s (32 bytes), recovery_id (1 byte)]
pub fn signature(&self) -> [u8; 65] {
let mut signature = [0u8; 65];
let r_bytes = self.r.to_big_endian();
let s_bytes = self.s.to_big_endian();
signature[..32].copy_from_slice(&r_bytes);
signature[32..64].copy_from_slice(&s_bytes);
signature[64] = self.y_parity.low_u32() as u8;
signature
}
}

#[derive(
Debug,
Clone,
Expand Down
22 changes: 17 additions & 5 deletions substrate/frame/revive/src/evm/api/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ use super::*;
use sp_core::{H160, U256};
use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};

/// Recover an Ethereum address from a message hash and signature.
///
/// # Parameters
/// - `message`: The message bytes to hash
/// - `signature`: The 65-byte ECDSA signature (r, s, v)
///
/// # Returns
/// The recovered Ethereum address, or an error if recovery fails
pub fn recover_eth_address_from_message(message: &[u8], signature: &[u8; 65]) -> Result<H160, ()> {
let hash = keccak_256(message);
let pk = secp256k1_ecdsa_recover(signature, &hash).map_err(|_| ())?;
let mut addr = H160::default();
addr.assign_from_slice(&keccak_256(&pk[..])[12..]);
Ok(addr)
}

impl TransactionLegacySigned {
/// Get the recovery ID from the signed transaction.
/// See https://eips.ethereum.org/EIPS/eip-155
Expand Down Expand Up @@ -165,11 +181,7 @@ impl TransactionSigned {
let bytes = s.out().to_vec();
let signature = self.raw_signature()?;

let hash = keccak_256(&bytes);
let mut addr = H160::default();
let pk = secp256k1_ecdsa_recover(&signature, &hash).map_err(|_| ())?;
addr.assign_from_slice(&keccak_256(&pk[..])[12..]);
Ok(addr)
recover_eth_address_from_message(&bytes, &signature)
}
}

Expand Down
23 changes: 22 additions & 1 deletion substrate/frame/revive/src/evm/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::{
evm::{
fees::{compute_max_integer_quotient, InfoT},
runtime::SetWeightLimit,
TYPE_LEGACY,
TYPE_EIP7702, TYPE_LEGACY,
},
extract_code_and_data, BalanceOf, CallOf, Config, GenericTransaction, Pallet, Weight, Zero,
LOG_TARGET, RUNTIME_PALLETS_ADDR,
Expand All @@ -48,6 +48,8 @@ pub struct CallInfo<T: Config> {
pub storage_deposit: BalanceOf<T>,
/// The ethereum gas limit of the transaction.
pub eth_gas_limit: U256,
/// EIP-7702: List of authorization tuples to process
pub authorization_list: Vec<crate::evm::SignedAuthorizationListEntry>,
}

/// Mode for creating a call from an ethereum transaction.
Expand Down Expand Up @@ -96,6 +98,22 @@ impl GenericTransaction {
return Err(InvalidTransaction::Call);
};

// EIP-7702: Validate that type 0x04 transactions have a non-null destination
// Per spec: "Note, this implies a null destination is not valid."
if let Some(super::Byte(TYPE_EIP7702)) = self.r#type.as_ref() {
if self.to.is_none() {
log::debug!(target: LOG_TARGET, "EIP-7702 transactions require non-null destination");
return Err(InvalidTransaction::Call);
}

// EIP-7702: Validate that type 0x04 transactions have non-empty authorization list
// Per spec: "The transaction is considered invalid if the length of authorization_list is zero."
if self.authorization_list.is_empty() {
log::debug!(target: LOG_TARGET, "EIP-7702 transactions require non-empty authorization list");
return Err(InvalidTransaction::Call);
}
}

// Currently, effective_gas_price will always be the same as base_fee
// Because all callers of `into_call` will prepare `tx` that way. Some of the subsequent
// logic will not work correctly anymore if we change that assumption.
Expand Down Expand Up @@ -164,6 +182,7 @@ impl GenericTransaction {
transaction_encoded,
effective_gas_price,
encoded_len,
authorization_list: self.authorization_list.clone(),
}
.into();
call
Expand Down Expand Up @@ -195,6 +214,7 @@ impl GenericTransaction {
};

// the fee as signed off by the eth wallet. we cannot consume more.
// EIP-7702: The gas limit already includes authorization processing costs
let eth_fee =
effective_gas_price.saturating_mul(gas) / <T as Config>::NativeToEthRatio::get();

Expand Down Expand Up @@ -252,6 +272,7 @@ impl GenericTransaction {
tx_fee,
storage_deposit,
eth_gas_limit: gas,
authorization_list: self.authorization_list.clone(),
})
}
}
Loading
Loading