Skip to content

Commit 59ff866

Browse files
authored
Add authorizing_address getter to AuthorizationListItem (#64)
* feat: ✨ add authorizing_address getter to AuthorizationListItem * test: ✅ cleanup tests * refactor: 🔥 do not reimplement ecrecover * feat: ✨ make authorization_message_hash public * style: 🎨 use module level use statements
1 parent a21785d commit 59ff866

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ bytes = { version = "1.0", default-features = false }
1313
ethereum-types = { version = "0.15", default-features = false, features = ["rlp", "codec"] }
1414
hash-db = { version = "0.16", default-features = false }
1515
hash256-std-hasher = { version = "0.15", default-features = false }
16+
k256 = { version = "0.13", default-features = false, features = ["ecdsa", "sha256"] }
1617
rlp = { version = "0.6", default-features = false, features = ["derive"] }
1718
sha3 = { version = "0.10", default-features = false }
1819
trie-root = { version = "0.18", default-features = false }
@@ -36,6 +37,7 @@ std = [
3637
"ethereum-types/std",
3738
"hash-db/std",
3839
"hash256-std-hasher/std",
40+
"k256/std",
3941
"rlp/std",
4042
"sha3/std",
4143
"trie-root/std",

src/transaction/eip7702.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use alloc::vec::Vec;
22

33
use ethereum_types::{Address, H256, U256};
4+
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
45
use rlp::{DecoderError, Rlp, RlpStream};
56
use sha3::{Digest, Keccak256};
67

@@ -9,6 +10,20 @@ use crate::{
910
Bytes,
1011
};
1112

13+
/// Error type for EIP-7702 authorization signature recovery
14+
#[derive(Debug, Clone, PartialEq, Eq)]
15+
#[cfg_attr(feature = "with-serde", derive(serde::Serialize, serde::Deserialize))]
16+
pub enum AuthorizationError {
17+
/// Invalid signature format
18+
InvalidSignature,
19+
/// Invalid recovery ID
20+
InvalidRecoveryId,
21+
/// Signature recovery failed
22+
RecoveryFailed,
23+
/// Invalid public key format
24+
InvalidPublicKey,
25+
}
26+
1227
/// EIP-7702 transaction type as defined in the specification
1328
pub const SET_CODE_TX_TYPE: u8 = 0x04;
1429

@@ -64,6 +79,72 @@ impl rlp::Decodable for AuthorizationListItem {
6479
}
6580
}
6681

82+
impl AuthorizationListItem {
83+
/// Recover the authorizing address from the authorization signature according to EIP-7702
84+
pub fn authorizing_address(&self) -> Result<Address, AuthorizationError> {
85+
// Create the authorization message hash according to EIP-7702
86+
let message_hash = self.authorization_message_hash();
87+
88+
// Create signature from r and s components
89+
let mut signature_bytes = [0u8; 64];
90+
signature_bytes[0..32].copy_from_slice(&self.r[..]);
91+
signature_bytes[32..64].copy_from_slice(&self.s[..]);
92+
93+
// Create the signature and recovery ID
94+
let signature = Signature::from_bytes(&signature_bytes.into())
95+
.map_err(|_| AuthorizationError::InvalidSignature)?;
96+
97+
let recovery_id = RecoveryId::try_from(if self.y_parity { 1u8 } else { 0u8 })
98+
.map_err(|_| AuthorizationError::InvalidRecoveryId)?;
99+
100+
// Recover the verifying key using VerifyingKey::recover_from_prehash
101+
// message_hash is already a 32-byte Keccak256 hash, so we use recover_from_prehash
102+
let verifying_key =
103+
VerifyingKey::recover_from_prehash(message_hash.as_bytes(), &signature, recovery_id)
104+
.map_err(|_| AuthorizationError::RecoveryFailed)?;
105+
106+
// Convert public key to Ethereum address
107+
Self::verifying_key_to_address(&verifying_key)
108+
}
109+
110+
/// Create the authorization message hash according to EIP-7702
111+
pub fn authorization_message_hash(&self) -> H256 {
112+
// EIP-7702 authorization message format:
113+
// MAGIC || rlp([chain_id, address, nonce])
114+
let mut message = alloc::vec![AUTHORIZATION_MAGIC];
115+
116+
// RLP encode the authorization tuple
117+
let mut rlp_stream = RlpStream::new_list(3);
118+
rlp_stream.append(&self.chain_id);
119+
rlp_stream.append(&self.address);
120+
rlp_stream.append(&self.nonce);
121+
message.extend_from_slice(&rlp_stream.out());
122+
123+
// Return keccak256 hash of the complete message
124+
H256::from_slice(Keccak256::digest(&message).as_slice())
125+
}
126+
127+
/// Convert VerifyingKey to Ethereum address
128+
fn verifying_key_to_address(
129+
verifying_key: &VerifyingKey,
130+
) -> Result<Address, AuthorizationError> {
131+
// Convert public key to bytes (uncompressed format, skip the 0x04 prefix)
132+
let pubkey_point = verifying_key.to_encoded_point(false);
133+
let pubkey_bytes = pubkey_point.as_bytes();
134+
135+
// pubkey_bytes is 65 bytes: [0x04, x_coord (32 bytes), y_coord (32 bytes)]
136+
// We want just the x and y coordinates (64 bytes total)
137+
if pubkey_bytes.len() >= 65 && pubkey_bytes[0] == 0x04 {
138+
let pubkey_coords = &pubkey_bytes[1..65];
139+
// Ethereum address is the last 20 bytes of keccak256(pubkey)
140+
let hash = Keccak256::digest(pubkey_coords);
141+
Ok(Address::from_slice(&hash[12..]))
142+
} else {
143+
Err(AuthorizationError::InvalidPublicKey)
144+
}
145+
}
146+
}
147+
67148
pub type AuthorizationList = Vec<AuthorizationListItem>;
68149

69150
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -206,3 +287,118 @@ impl From<EIP7702Transaction> for EIP7702TransactionMessage {
206287
t.to_message()
207288
}
208289
}
290+
291+
#[cfg(test)]
292+
mod tests {
293+
use super::*;
294+
use ethereum_types::{Address, H256, U256};
295+
296+
#[test]
297+
fn test_authorizing_address_with_real_signature() {
298+
use k256::ecdsa::SigningKey;
299+
use k256::elliptic_curve::SecretKey;
300+
301+
// Use a fixed test private key for deterministic testing
302+
let private_key_bytes = [
303+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
304+
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
305+
0x1d, 0x1e, 0x1f, 0x20,
306+
];
307+
308+
let secret_key =
309+
SecretKey::from_bytes(&private_key_bytes.into()).expect("Invalid private key");
310+
let signing_key = SigningKey::from(secret_key);
311+
let verifying_key = signing_key.verifying_key();
312+
313+
// Create authorization data
314+
let chain_id = 1u64;
315+
let address = Address::from_slice(&[0x42u8; 20]);
316+
let nonce = U256::zero();
317+
318+
// Create the EIP-7702 authorization message hash
319+
let mut message = alloc::vec![AUTHORIZATION_MAGIC];
320+
let mut rlp_stream = RlpStream::new_list(3);
321+
rlp_stream.append(&chain_id);
322+
rlp_stream.append(&address);
323+
rlp_stream.append(&nonce);
324+
message.extend_from_slice(&rlp_stream.out());
325+
let message_hash = H256::from_slice(Keccak256::digest(&message).as_slice());
326+
327+
// Sign the message hash
328+
let (signature, recovery_id) = signing_key
329+
.sign_prehash_recoverable(message_hash.as_bytes())
330+
.expect("Failed to sign message");
331+
332+
// Extract signature components
333+
let signature_bytes = signature.to_bytes();
334+
let r = H256::from_slice(&signature_bytes[0..32]);
335+
let s = H256::from_slice(&signature_bytes[32..64]);
336+
let y_parity = recovery_id.is_y_odd();
337+
338+
// Create AuthorizationListItem with real signature
339+
let auth_item = AuthorizationListItem {
340+
chain_id,
341+
address,
342+
nonce,
343+
y_parity,
344+
r,
345+
s,
346+
};
347+
348+
// Recover the authorizing address
349+
let recovered_address = auth_item
350+
.authorizing_address()
351+
.expect("Failed to recover authorizing address");
352+
353+
// Convert the original verifying key to an Ethereum address for comparison
354+
let expected_address = AuthorizationListItem::verifying_key_to_address(&verifying_key)
355+
.expect("Failed to convert verifying key to address");
356+
357+
// Verify that the recovered address matches the original signer
358+
assert_eq!(recovered_address, expected_address);
359+
assert_ne!(recovered_address, Address::zero());
360+
361+
// For deterministic testing, verify specific expected values
362+
// This ensures the implementation is working correctly with known inputs
363+
assert_eq!(
364+
expected_address,
365+
Address::from_slice(&hex_literal::hex!(
366+
"6370ef2f4db3611d657b90667de398a2cc2a370c"
367+
))
368+
);
369+
}
370+
371+
#[test]
372+
fn test_authorizing_address_error_handling() {
373+
// Test with invalid signature components (zero values are invalid in ECDSA)
374+
let auth_item = AuthorizationListItem {
375+
chain_id: 1,
376+
address: Address::from_slice(&[0x42u8; 20]),
377+
nonce: U256::zero(),
378+
y_parity: false,
379+
r: H256::zero(), // Invalid r value (r cannot be zero)
380+
s: H256::zero(), // Invalid s value (s cannot be zero)
381+
};
382+
383+
// This should return an error due to invalid signature
384+
let result = auth_item.authorizing_address();
385+
assert!(result.is_err());
386+
assert_eq!(result.unwrap_err(), AuthorizationError::InvalidSignature);
387+
388+
// Test with values that are too high (greater than secp256k1 curve order)
389+
// secp256k1 curve order is FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
390+
let auth_item_high_values = AuthorizationListItem {
391+
chain_id: 1,
392+
address: Address::from_slice(&[0x42u8; 20]),
393+
nonce: U256::zero(),
394+
y_parity: false,
395+
// Use maximum possible values which exceed the curve order
396+
r: H256::from_slice(&[0xFF; 32]),
397+
s: H256::from_slice(&[0xFF; 32]),
398+
};
399+
400+
let result = auth_item_high_values.authorizing_address();
401+
assert!(result.is_err());
402+
assert_eq!(result.unwrap_err(), AuthorizationError::InvalidSignature);
403+
}
404+
}

0 commit comments

Comments
 (0)