diff --git a/crates/alloy/Cargo.toml b/crates/alloy/Cargo.toml index d2da45e5c4a..c925959a88d 100644 --- a/crates/alloy/Cargo.toml +++ b/crates/alloy/Cargo.toml @@ -85,10 +85,10 @@ full = [ "kzg", "network", "provider-http", # includes `providers` - "provider-ws", # includes `providers` - "provider-ipc", # includes `providers` - "rpc-types", # includes `rpc-types-eth` - "signer-local", # includes `signers` + "provider-ws", # includes `providers` + "provider-ipc", # includes `providers` + "rpc-types", # includes `rpc-types-eth` + "signer-local", # includes `signers` ] # configuration @@ -218,6 +218,7 @@ arbitrary = [ k256 = [ "alloy-core/k256", "alloy-consensus?/k256", + "alloy-eips?/k256", "alloy-network?/k256", "alloy-rpc-types?/k256", ] diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index beeb8adfa9b..1bc962c4911 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -50,7 +50,7 @@ serde_json.workspace = true [features] default = ["std"] std = ["alloy-eips/std", "c-kzg?/std"] -k256 = ["alloy-primitives/k256"] +k256 = ["alloy-primitives/k256", "alloy-eips/k256"] kzg = ["dep:c-kzg", "alloy-eips/kzg", "std"] arbitrary = [ "std", diff --git a/crates/eips/Cargo.toml b/crates/eips/Cargo.toml index 08c9a303233..26f1c293264 100644 --- a/crates/eips/Cargo.toml +++ b/crates/eips/Cargo.toml @@ -70,6 +70,7 @@ serde = [ kzg = ["kzg-sidecar", "sha2", "dep:derive_more", "dep:c-kzg", "dep:once_cell"] kzg-sidecar = ["sha2"] sha2 = ["dep:sha2"] +k256 = ["alloy-primitives/k256"] ssz = [ "std", "dep:ethereum_ssz", diff --git a/crates/eips/README.md b/crates/eips/README.md index f516218b417..4956250ee6c 100644 --- a/crates/eips/README.md +++ b/crates/eips/README.md @@ -18,3 +18,4 @@ Contains constants, helpers, and basic data structures for consensus EIPs. - [EIP-7002](https://eips.ethereum.org/EIPS/eip-7002) - [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251) - [EIP-7685](https://eips.ethereum.org/EIPS/eip-7685) +- [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) diff --git a/crates/eips/src/eip6110.rs b/crates/eips/src/eip6110.rs index 0f39db8ad10..f04c02d1897 100644 --- a/crates/eips/src/eip6110.rs +++ b/crates/eips/src/eip6110.rs @@ -1,4 +1,4 @@ -//! Contains Deposit types, first introduced in the Prague hardfork: +//! Contains Deposit types, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md). //! //! See also [EIP-6110](https://eips.ethereum.org/EIPS/eip-6110): Supply validator deposits on chain //! diff --git a/crates/eips/src/eip7251.rs b/crates/eips/src/eip7251.rs index 27c51210345..c62c2c641c4 100644 --- a/crates/eips/src/eip7251.rs +++ b/crates/eips/src/eip7251.rs @@ -1,4 +1,4 @@ -//! Contains consolidtion types, first introduced in the Prague hardfork: +//! Contains consolidtion types, first introduced in the [Prague hardfork](https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md). //! //! See also [EIP-7251](https://eips.ethereum.org/EIPS/eip-7251): Increase the MAX_EFFECTIVE_BALANCE diff --git a/crates/eips/src/eip7702/auth_list.rs b/crates/eips/src/eip7702/auth_list.rs new file mode 100644 index 00000000000..c6c05ce81d0 --- /dev/null +++ b/crates/eips/src/eip7702/auth_list.rs @@ -0,0 +1,237 @@ +use core::ops::Deref; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use alloy_primitives::{keccak256, Address, ChainId, B256}; +use alloy_rlp::{BufMut, Decodable, Encodable, Header, RlpDecodable, RlpEncodable}; + +/// An unsigned EIP-7702 authorization. +#[derive(Debug, Clone, RlpEncodable, RlpDecodable, Eq, PartialEq)] +pub struct Authorization { + /// The chain ID of the authorization. + pub chain_id: ChainId, + /// The address of the authorization. + pub address: Address, + /// The nonce for the authorization. + pub nonce: OptionalNonce, +} + +impl Authorization { + /// Get the `chain_id` for the authorization. + /// + /// # Note + /// + /// Implementers should check that this matches the current `chain_id` *or* is 0. + pub const fn chain_id(&self) -> ChainId { + self.chain_id + } + + /// Get the `address` for the authorization. + pub const fn address(&self) -> &Address { + &self.address + } + + /// Get the `nonce` for the authorization. + /// + /// # Note + /// + /// If this is `Some`, implementers should check that the nonce of the authority is equal to + /// this nonce. + pub fn nonce(&self) -> Option { + *self.nonce + } + + /// Computes the signature hash used to sign the authorization, or recover the authority from a + /// signed authorization list item. + /// + /// The signature hash is `keccak(MAGIC || rlp([chain_id, [nonce], address]))` + #[inline] + pub fn signature_hash(&self) -> B256 { + use super::constants::MAGIC; + + #[derive(RlpEncodable)] + struct Auth { + chain_id: ChainId, + nonce: OptionalNonce, + address: Address, + } + + let mut buf = Vec::new(); + buf.put_u8(MAGIC); + + Auth { chain_id: self.chain_id, nonce: self.nonce, address: self.address }.encode(&mut buf); + + keccak256(buf) + } + + /// Convert to a signed authorization by adding a signature. + pub const fn into_signed(self, signature: S) -> SignedAuthorization { + SignedAuthorization { inner: self, signature } + } +} + +/// A signed EIP-7702 authorization. +#[derive(Debug, Clone, RlpEncodable, RlpDecodable)] +pub struct SignedAuthorization { + inner: Authorization, + signature: S, +} + +impl SignedAuthorization { + /// Get the `signature` for the authorization. + pub const fn signature(&self) -> &S { + &self.signature + } +} + +#[cfg(feature = "k256")] +impl SignedAuthorization { + /// Recover the authority for the authorization. + /// + /// # Note + /// + /// Implementers should check that the authority has no code. + pub fn recover_authority(&self) -> Result { + self.signature.recover_address_from_prehash(&self.inner.signature_hash()) + } + + /// Recover the authority and transform the signed authorization into a + /// [`RecoveredAuthorization`]. + pub fn into_recovered(self) -> RecoveredAuthorization { + let authority = self.recover_authority().ok(); + RecoveredAuthorization { inner: self.inner, authority } + } +} + +impl Deref for SignedAuthorization { + type Target = Authorization; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +/// A recovered authorization. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RecoveredAuthorization { + inner: Authorization, + authority: Option
, +} + +impl RecoveredAuthorization { + /// Get the `authority` for the authorization. + /// + /// If this is `None`, then the authority could not be recovered. + pub const fn authority(&self) -> Option
{ + self.authority + } +} + +impl Deref for RecoveredAuthorization { + type Target = Authorization; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +/// An internal wrapper around an `Option` for optional nonces. +/// +/// In EIP-7702 the nonce is encoded as a list of either 0 or 1 items, where 0 items means that no +/// nonce was specified (i.e. `None`). If there is 1 item, this is the same as `Some`. +/// +/// The wrapper type is used for RLP encoding and decoding. +#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] +pub struct OptionalNonce(Option); + +impl OptionalNonce { + /// Create a new [`OptionalNonce`] + pub const fn new(nonce: Option) -> Self { + Self(nonce) + } +} + +impl From> for OptionalNonce { + fn from(value: Option) -> Self { + Self::new(value) + } +} + +impl Encodable for OptionalNonce { + fn encode(&self, out: &mut dyn BufMut) { + match self.0 { + Some(nonce) => { + Header { list: true, payload_length: nonce.length() }.encode(out); + nonce.encode(out); + } + None => Header { list: true, payload_length: 0 }.encode(out), + } + } +} + +impl Decodable for OptionalNonce { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let mut bytes = Header::decode_bytes(buf, true)?; + if bytes.is_empty() { + return Ok(Self(None)); + } + + let payload_view = &mut bytes; + let nonce = u64::decode(payload_view)?; + if !payload_view.is_empty() { + // if there's more than 1 item in the nonce list we error + Err(alloy_rlp::Error::UnexpectedLength) + } else { + Ok(Self(Some(nonce))) + } + } +} + +impl Deref for OptionalNonce { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_encode_decode_roundtrip(auth: Authorization) { + let mut buf = Vec::new(); + auth.encode(&mut buf); + let decoded = Authorization::decode(&mut buf.as_ref()).unwrap(); + assert_eq!(buf.len(), auth.length()); + assert_eq!(decoded, auth); + } + + #[test] + fn test_encode_decode_auth() { + // fully filled + test_encode_decode_roundtrip(Authorization { + chain_id: 1u64, + address: Address::left_padding_from(&[6]), + nonce: Some(1u64).into(), + }); + + // no nonce + test_encode_decode_roundtrip(Authorization { + chain_id: 1u64, + address: Address::left_padding_from(&[6]), + nonce: None.into(), + }); + } + + #[test] + fn opt_nonce_too_many_elements() { + let mut buf = Vec::new(); + vec![1u64, 2u64].encode(&mut buf); + + assert_eq!( + OptionalNonce::decode(&mut buf.as_ref()), + Err(alloy_rlp::Error::UnexpectedLength) + ) + } +} diff --git a/crates/eips/src/eip7702/constants.rs b/crates/eips/src/eip7702/constants.rs new file mode 100644 index 00000000000..18b0453f514 --- /dev/null +++ b/crates/eips/src/eip7702/constants.rs @@ -0,0 +1,18 @@ +//! [EIP-7702] constants. +//! +//! [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + +/// Identifier for EIP7702's set code transaction. +/// +/// See also [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). +pub const EIP7702_TX_TYPE_ID: u8 = 4; + +/// Magic number used to calculate an EIP7702 authority. +/// +/// See also [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). +pub const MAGIC: u8 = 0x05; + +/// An additional gas cost per EIP7702 authorization list item. +/// +/// See also [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702). +pub const PER_AUTH_BASE_COST: u64 = 2500; diff --git a/crates/eips/src/eip7702/mod.rs b/crates/eips/src/eip7702/mod.rs new file mode 100644 index 00000000000..26ad27a883c --- /dev/null +++ b/crates/eips/src/eip7702/mod.rs @@ -0,0 +1,8 @@ +//! [EIP-7702] constants, helpers, and types. +//! +//! [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 + +mod auth_list; +pub use auth_list::*; + +pub mod constants; diff --git a/crates/eips/src/lib.rs b/crates/eips/src/lib.rs index 0ad80222275..393ba4f343e 100644 --- a/crates/eips/src/lib.rs +++ b/crates/eips/src/lib.rs @@ -27,11 +27,11 @@ pub mod eip2935; pub mod eip4788; -pub mod eip4895; - pub mod eip4844; pub use eip4844::{calc_blob_gasprice, calc_excess_blob_gas}; +pub mod eip4895; + pub mod eip6110; pub mod merge; @@ -40,3 +40,5 @@ pub mod eip7002; pub mod eip7251; pub mod eip7685; + +pub mod eip7702; diff --git a/crates/rpc-types-eth/Cargo.toml b/crates/rpc-types-eth/Cargo.toml index 63bf186f99d..2e2b9a759e6 100644 --- a/crates/rpc-types-eth/Cargo.toml +++ b/crates/rpc-types-eth/Cargo.toml @@ -66,4 +66,4 @@ arbitrary = [ ] jsonrpsee-types = ["dep:jsonrpsee-types"] ssz = ["alloy-primitives/ssz", "alloy-eips/ssz"] -k256 = ["alloy-consensus/k256"] +k256 = ["alloy-consensus/k256", "alloy-eips/k256"]