Skip to content

Commit

Permalink
feat(anvil): wallet_ namespace + inject P256BatchDelegation + exe…
Browse files Browse the repository at this point in the history
…cutor (#9110)

* feat(anvil-rpc): wallet_ namespace

* feat: init sponsor and delegation contract in backend

* wallet_sendTransaction

* wallet_sendTransaction

* update p256 runtime code

* nit P256_DELEGATION_CONTRACT addr

* use correct runtime codes

* fix

* doc nits

* fix

* feat: anvil_addCapability

* nit

* feat: anvil_setExecutor

* tests
  • Loading branch information
yash-atreya authored Oct 17, 2024
1 parent ca49147 commit 08021d9
Show file tree
Hide file tree
Showing 8 changed files with 445 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/anvil/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ bytes = "1.4"

# misc
rand = "0.8"
thiserror.workspace = true

[features]
default = ["serde"]
Expand Down
19 changes: 19 additions & 0 deletions crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod subscription;
pub mod transaction;
pub mod trie;
pub mod utils;
pub mod wallet;

#[cfg(feature = "serde")]
pub mod serde_helpers;
Expand Down Expand Up @@ -769,6 +770,24 @@ pub enum EthRequest {
/// Reorg the chain
#[cfg_attr(feature = "serde", serde(rename = "anvil_reorg",))]
Reorg(ReorgOptions),

/// Wallet
#[cfg_attr(feature = "serde", serde(rename = "wallet_getCapabilities", with = "empty_params"))]
WalletGetCapabilities(()),

/// Wallet send_tx
#[cfg_attr(feature = "serde", serde(rename = "wallet_sendTransaction", with = "sequence"))]
WalletSendTransaction(Box<WithOtherFields<TransactionRequest>>),

/// Add an address to the [`DelegationCapability`] of the wallet
///
/// [`DelegationCapability`]: wallet::DelegationCapability
#[cfg_attr(feature = "serde", serde(rename = "anvil_addCapability", with = "sequence"))]
AnvilAddCapability(Address),

/// Set the executor (sponsor) wallet
#[cfg_attr(feature = "serde", serde(rename = "anvil_setExecutor", with = "sequence"))]
AnvilSetExecutor(String),
}

/// Represents ethereum JSON-RPC API
Expand Down
79 changes: 79 additions & 0 deletions crates/anvil/core/src/eth/wallet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use alloy_primitives::{map::HashMap, Address, ChainId, U64};
use serde::{Deserialize, Serialize};

/// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the sequencer.
///
/// The sequencer will only perform delegations, and act on behalf of delegated accounts, if the
/// account delegates to one of the addresses specified within this capability.
///
/// [eip-7702]: https://eips.ethereum.org/EIPS/eip-7702
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Default)]
pub struct DelegationCapability {
/// A list of valid delegation contracts.
pub addresses: Vec<Address>,
}

/// Wallet capabilities for a specific chain.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Default)]
pub struct Capabilities {
/// The capability to delegate.
pub delegation: DelegationCapability,
}

/// A map of wallet capabilities per chain ID.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Default)]
pub struct WalletCapabilities(HashMap<U64, Capabilities>);

impl WalletCapabilities {
/// Get the capabilities of the wallet API for the specified chain ID.
pub fn get(&self, chain_id: ChainId) -> Option<&Capabilities> {
self.0.get(&U64::from(chain_id))
}

pub fn insert(&mut self, chain_id: ChainId, capabilities: Capabilities) {
self.0.insert(U64::from(chain_id), capabilities);
}
}

#[derive(Debug, thiserror::Error)]
pub enum WalletError {
/// The transaction value is not 0.
///
/// The value should be 0 to prevent draining the sequencer.
#[error("tx value not zero")]
ValueNotZero,
/// The from field is set on the transaction.
///
/// Requests with the from field are rejected, since it is implied that it will always be the
/// sequencer.
#[error("tx from field is set")]
FromSet,
/// The nonce field is set on the transaction.
///
/// Requests with the nonce field set are rejected, as this is managed by the sequencer.
#[error("tx nonce is set")]
NonceSet,
/// An authorization item was invalid.
///
/// The item is invalid if it tries to delegate an account to a contract that is not
/// whitelisted.
#[error("invalid authorization address")]
InvalidAuthorization,
/// The to field of the transaction was invalid.
///
/// The destination is invalid if:
///
/// - There is no bytecode at the destination, or
/// - The bytecode is not an EIP-7702 delegation designator, or
/// - The delegation designator points to a contract that is not whitelisted
#[error("the destination of the transaction is not a delegated account")]
IllegalDestination,
/// The transaction request was invalid.
///
/// This is likely an internal error, as most of the request is built by the sequencer.
#[error("invalid tx request")]
InvalidTransactionRequest,
/// An internal error occurred.
#[error("internal error")]
InternalError,
}
154 changes: 150 additions & 4 deletions crates/anvil/src/eth/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use super::{
};
use crate::{
eth::{
backend,
backend::{
self,
db::SerializableState,
mem::{MIN_CREATE_GAS, MIN_TRANSACTION_GAS},
notifications::NewBlockNotifications,
Expand All @@ -23,8 +23,7 @@ use crate::{
},
Pool,
},
sign,
sign::Signer,
sign::{self, Signer},
},
filter::{EthFilter, Filters, LogsFilter},
mem::transaction_build,
Expand All @@ -34,11 +33,17 @@ use crate::{
use alloy_consensus::{transaction::eip4844::TxEip4844Variant, Account, TxEnvelope};
use alloy_dyn_abi::TypedData;
use alloy_eips::eip2718::Encodable2718;
use alloy_network::{eip2718::Decodable2718, BlockResponse};
use alloy_network::{
eip2718::Decodable2718, BlockResponse, Ethereum, NetworkWallet, TransactionBuilder,
};
use alloy_primitives::{
map::{HashMap, HashSet},
Address, Bytes, Parity, TxHash, TxKind, B256, B64, U256, U64,
};
use alloy_provider::utils::{
eip1559_default_estimator, EIP1559_FEE_ESTIMATION_PAST_BLOCKS,
EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE,
};
use alloy_rpc_types::{
anvil::{
ForkedNetwork, Forking, Metadata, MineOptions, NodeEnvironment, NodeForkConfig, NodeInfo,
Expand All @@ -65,6 +70,7 @@ use anvil_core::{
transaction_request_to_typed, PendingTransaction, ReceiptResponse, TypedTransaction,
TypedTransactionRequest,
},
wallet::{WalletCapabilities, WalletError},
EthRequest,
},
types::{ReorgOptions, TransactionData, Work},
Expand All @@ -82,6 +88,7 @@ use foundry_evm::{
};
use futures::channel::{mpsc::Receiver, oneshot};
use parking_lot::RwLock;
use revm::primitives::Bytecode;
use std::{future::Future, sync::Arc, time::Duration};

/// The client version: `anvil/v{major}.{minor}.{patch}`
Expand Down Expand Up @@ -449,6 +456,14 @@ impl EthApi {
EthRequest::Reorg(reorg_options) => {
self.anvil_reorg(reorg_options).await.to_rpc_result()
}
EthRequest::WalletGetCapabilities(()) => self.get_capabilities().to_rpc_result(),
EthRequest::WalletSendTransaction(tx) => {
self.wallet_send_transaction(*tx).await.to_rpc_result()
}
EthRequest::AnvilAddCapability(addr) => self.anvil_add_capability(addr).to_rpc_result(),
EthRequest::AnvilSetExecutor(executor_pk) => {
self.anvil_set_executor(executor_pk).to_rpc_result()
}
}
}

Expand Down Expand Up @@ -2369,6 +2384,137 @@ impl EthApi {
}
}

// ===== impl Wallet endppoints =====
impl EthApi {
/// Get the capabilities of the wallet.
///
/// See also [EIP-5792][eip-5792].
///
/// [eip-5792]: https://eips.ethereum.org/EIPS/eip-5792
pub fn get_capabilities(&self) -> Result<WalletCapabilities> {
node_info!("wallet_getCapabilities");
Ok(self.backend.get_capabilities())
}

pub async fn wallet_send_transaction(
&self,
mut request: WithOtherFields<TransactionRequest>,
) -> Result<TxHash> {
node_info!("wallet_sendTransaction");

// Validate the request
// reject transactions that have a non-zero value to prevent draining the executor.
if request.value.is_some_and(|val| val > U256::ZERO) {
return Err(WalletError::ValueNotZero.into())
}

// reject transactions that have from set, as this will be the executor.
if request.from.is_some() {
return Err(WalletError::FromSet.into());
}

// reject transaction requests that have nonce set, as this is managed by the executor.
if request.nonce.is_some() {
return Err(WalletError::NonceSet.into());
}

let capabilities = self.backend.get_capabilities();
let valid_delegations: &[Address] = capabilities
.get(self.chain_id())
.map(|caps| caps.delegation.addresses.as_ref())
.unwrap_or_default();

if let Some(authorizations) = &request.authorization_list {
if authorizations.iter().any(|auth| !valid_delegations.contains(&auth.address)) {
return Err(WalletError::InvalidAuthorization.into());
}
}

// validate the destination address
match (request.authorization_list.is_some(), request.to) {
// if this is an eip-1559 tx, ensure that it is an account that delegates to a
// whitelisted address
(false, Some(TxKind::Call(addr))) => {
let acc = self.backend.get_account(addr).await?;

let delegated_address = acc
.code
.map(|code| match code {
Bytecode::Eip7702(c) => c.address(),
_ => Address::ZERO,
})
.unwrap_or_default();

// not a whitelisted address, or not an eip-7702 bytecode
if delegated_address == Address::ZERO ||
!valid_delegations.contains(&delegated_address)
{
return Err(WalletError::IllegalDestination.into());
}
}
// if it's an eip-7702 tx, let it through
(true, _) => (),
// create tx's disallowed
_ => return Err(WalletError::IllegalDestination.into()),
}

let wallet = self.backend.executor_wallet().ok_or(WalletError::InternalError)?;

let from = NetworkWallet::<Ethereum>::default_signer_address(&wallet);

let nonce = self.get_transaction_count(from, Some(BlockId::latest())).await?;

request.nonce = Some(nonce);

let chain_id = self.chain_id();

request.chain_id = Some(chain_id);

request.from = Some(from);

let gas_limit_fut = self.estimate_gas(request.clone(), Some(BlockId::latest()), None);

let fees_fut = self.fee_history(
U256::from(EIP1559_FEE_ESTIMATION_PAST_BLOCKS),
BlockNumber::Latest,
vec![EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE],
);

let (gas_limit, fees) = tokio::join!(gas_limit_fut, fees_fut);

let gas_limit = gas_limit?;
let fees = fees?;

request.gas = Some(gas_limit.to());

let base_fee = fees.latest_block_base_fee().unwrap_or_default();

let estimation = eip1559_default_estimator(base_fee, &fees.reward.unwrap_or_default());

request.max_fee_per_gas = Some(estimation.max_fee_per_gas);
request.max_priority_fee_per_gas = Some(estimation.max_priority_fee_per_gas);
request.gas_price = None;

let envelope = request.build(&wallet).await.map_err(|_| WalletError::InternalError)?;

self.send_raw_transaction(envelope.encoded_2718().into()).await
}

/// Add an address to the delegation capability of wallet.
///
/// This entails that the executor will now be able to sponsor transactions to this address.
pub fn anvil_add_capability(&self, address: Address) -> Result<()> {
node_info!("anvil_addCapability");
self.backend.add_capability(address);
Ok(())
}

pub fn anvil_set_executor(&self, executor_pk: String) -> Result<Address> {
node_info!("anvil_setExecutor");
self.backend.set_executor(executor_pk)
}
}

impl EthApi {
/// Executes the future on a new blocking task.
async fn on_blocking_task<C, F, R>(&self, c: C) -> Result<R>
Expand Down
Loading

0 comments on commit 08021d9

Please sign in to comment.