Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anvil): wallet_ namespace + inject P256BatchDelegation + executor #9110

Merged
merged 17 commits into from
Oct 17, 2024
Merged
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
Loading