Skip to content

Commit

Permalink
feat: add eip-7702 helpers (#950)
Browse files Browse the repository at this point in the history
* feat: add eip-7702 auth list types

* chore: fmt

* chore: no_std

* feat: add k256 feature to alloy-eips

* chore: const fns

* chore: lint

* chore: rm vec type alias

* feat: split into signed/unsigned types

* fix: use `signature_prehash`

* chore: fmt

* chore: const fn

* docs: fix doc link

* chore: fmt

-.-

* chore: make pub

* chore: cleanup

* fix: error on list longer than 1 for nonce

* chore: const fn -.-

* feat: default derive

* chore: manual decode impl for `OptionalNonce`

* docs: small driveby link nits

* test: add small rt ser/de tests

* feat: add `RecoveredAuthorization`

* fix: partial move
  • Loading branch information
onbjerg authored Jun 24, 2024
1 parent ea0cffc commit 2d26b05
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 10 deletions.
9 changes: 5 additions & 4 deletions crates/alloy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -263,6 +263,7 @@ arbitrary = [
k256 = [
"alloy-core/k256",
"alloy-consensus?/k256",
"alloy-eips?/k256",
"alloy-network?/k256",
"alloy-rpc-types?/k256",
]
Expand Down
2 changes: 1 addition & 1 deletion crates/consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/eips/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/eips/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion crates/eips/src/eip6110.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Contains Deposit types, first introduced in the Prague hardfork: <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md>
//! 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
//!
Expand Down
2 changes: 1 addition & 1 deletion crates/eips/src/eip7251.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Contains consolidtion types, first introduced in the Prague hardfork: <https://github.com/ethereum/execution-apis/blob/main/src/engine/prague.md>
//! 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
Expand Down
237 changes: 237 additions & 0 deletions crates/eips/src/eip7702/auth_list.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
*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<S>(self, signature: S) -> SignedAuthorization<S> {
SignedAuthorization { inner: self, signature }
}
}

/// A signed EIP-7702 authorization.
#[derive(Debug, Clone, RlpEncodable, RlpDecodable)]
pub struct SignedAuthorization<S> {
inner: Authorization,
signature: S,
}

impl<S> SignedAuthorization<S> {
/// Get the `signature` for the authorization.
pub const fn signature(&self) -> &S {
&self.signature
}
}

#[cfg(feature = "k256")]
impl SignedAuthorization<alloy_primitives::Signature> {
/// Recover the authority for the authorization.
///
/// # Note
///
/// Implementers should check that the authority has no code.
pub fn recover_authority(&self) -> Result<Address, alloy_primitives::SignatureError> {
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<S> Deref for SignedAuthorization<S> {
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<Address>,
}

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<Address> {
self.authority
}
}

impl Deref for RecoveredAuthorization {
type Target = Authorization;

fn deref(&self) -> &Self::Target {
&self.inner
}
}

/// An internal wrapper around an `Option<u64>` 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<u64>);

impl OptionalNonce {
/// Create a new [`OptionalNonce`]
pub const fn new(nonce: Option<u64>) -> Self {
Self(nonce)
}
}

impl From<Option<u64>> for OptionalNonce {
fn from(value: Option<u64>) -> 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<Self> {
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<u64>;

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)
)
}
}
18 changes: 18 additions & 0 deletions crates/eips/src/eip7702/constants.rs
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions crates/eips/src/eip7702/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 4 additions & 2 deletions crates/eips/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -40,3 +40,5 @@ pub mod eip7002;
pub mod eip7251;

pub mod eip7685;

pub mod eip7702;
2 changes: 1 addition & 1 deletion crates/rpc-types-eth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

0 comments on commit 2d26b05

Please sign in to comment.