Skip to content

Commit

Permalink
feat: impl wallet_ namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
onbjerg committed Oct 8, 2024
1 parent fa3f2e7 commit 5b7da3f
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 4 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ reth-chainspec = { git = "https://github.com/paradigmxyz/reth.git", rev = "78426
] }
reth-cli = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-cli-util = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [
"optimism",
] }
reth-node-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-node-builder = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-node-core = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [
Expand All @@ -106,6 +109,7 @@ reth-provider = { git = "https://github.com/paradigmxyz/reth.git", rev = "784267
reth-revm = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [
"optimism",
] }
reth-storage-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }

Expand All @@ -119,6 +123,7 @@ tracing = "0.1.0"
serde = "1"
serde_json = "1"
once_cell = "1.19"
thiserror = "1"

# misc-testing
rstest = "0.18.2"
5 changes: 5 additions & 0 deletions bin/alphanet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ default-run = "alphanet"
workspace = true

[dependencies]
alloy-signer-local.workspace = true
alloy-network.workspace = true
alloy-primitives.workspace = true
alphanet-node.workspace = true
alphanet-wallet.workspace = true
eyre.workspace = true
tracing.workspace = true
reth-cli-util.workspace = true
reth-node-builder.workspace = true
Expand Down
36 changes: 36 additions & 0 deletions bin/alphanet/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@
//! - `min-debug-logs`: Disables all logs below `debug` level.
//! - `min-trace-logs`: Disables all logs below `trace` level.

use alloy_network::EthereumWallet;
use alloy_primitives::Address;
use alloy_signer_local::PrivateKeySigner;
use alphanet_node::{chainspec::AlphanetChainSpecParser, node::AlphaNetNode};
use alphanet_wallet::{AlphaNetWallet, AlphaNetWalletApiServer};
use clap::Parser;
use eyre::Context;
use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher};
use reth_optimism_cli::Cli;
use reth_optimism_node::{args::RollupArgs, node::OptimismAddOns};
use reth_optimism_rpc::sequencer::SequencerClient;
use reth_provider::providers::BlockchainProvider2;
use tracing::{info, warn};

// We use jemalloc for performance reasons.
#[cfg(all(feature = "jemalloc", unix))]
Expand Down Expand Up @@ -60,6 +66,36 @@ fn main() {
.set_sequencer_client(SequencerClient::new(sequencer_http))?;
}

// register alphanet wallet namespace
if let Ok(sk) = std::env::var("EXP1_SK") {
let signer: PrivateKeySigner =
sk.parse().wrap_err("Invalid EXP0001 secret key.")?;
let wallet = EthereumWallet::from(signer);

let raw_delegations = std::env::var("EXP1_WHITELIST")
.wrap_err("No EXP0001 delegations specified")?;
let valid_delegations: Vec<Address> = raw_delegations
.split(',')
.map(|addr| Address::parse_checksummed(addr, None))
.collect::<Result<_, _>>()
.wrap_err("No valid EXP0001 delegations specified")?;

ctx.modules.merge_configured(
AlphaNetWallet::new(
ctx.provider().clone(),
wallet,
ctx.registry.eth_api().clone(),
ctx.config().chain.chain().id(),
valid_delegations,
)
.into_rpc(),
)?;

info!(target: "reth::cli", "EXP0001 wallet configured");
} else {
warn!(target: "reth::cli", "EXP0001 wallet not configured");
}

Ok(())
})
.launch_with_fn(|builder| {
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
msrv = "1.80"
msrv = "1.81"
allow-dbg-in-tests = true
6 changes: 6 additions & 0 deletions crates/wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ keywords.workspace = true
categories.workspace = true

[dependencies]
alloy-network.workspace = true
alloy-primitives.workspace = true
alloy-rpc-types.workspace = true
jsonrpsee = { workspace = true, features = ["server", "macros"] }
reth-primitives.workspace = true
reth-storage-api.workspace = true
reth-rpc-eth-api.workspace = true
serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true
tracing.workspace = true

[lints]
workspace = true
218 changes: 215 additions & 3 deletions crates/wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@

#![cfg_attr(not(test), warn(unused_crate_dependencies))]

use alloy_primitives::{map::HashMap, Address, ChainId, TxHash};
use alloy_network::{
eip2718::Encodable2718, Ethereum, EthereumWallet, NetworkWallet, TransactionBuilder,
};
use alloy_primitives::{map::HashMap, Address, ChainId, TxHash, TxKind, U256};
use alloy_rpc_types::TransactionRequest;
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
use jsonrpsee::{
core::{async_trait, RpcResult},
proc_macros::rpc,
};
use reth_primitives::{revm_primitives::Bytecode, BlockId};
use reth_rpc_eth_api::helpers::{EthCall, EthState, EthTransactions, FullEthApi};
use reth_storage_api::{StateProvider, StateProviderFactory};
use serde::{Deserialize, Serialize};
use tracing::trace;

/// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the sequencer.
///
Expand All @@ -45,6 +55,13 @@ pub struct Capabilities {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WalletCapabilities(pub HashMap<ChainId, 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(&chain_id)
}
}

/// AlphaNet `wallet_` RPC namespace.
#[cfg_attr(not(test), rpc(server, namespace = "wallet"))]
#[cfg_attr(test, rpc(server, client, namespace = "wallet"))]
Expand Down Expand Up @@ -75,5 +92,200 @@ pub trait AlphaNetWalletApi {
/// [eip-7702]: https://eips.ethereum.org/EIPS/eip-7702
/// [eip-1559]: https://eips.ethereum.org/EIPS/eip-1559
#[method(name = "sendTransaction")]
fn send_transaction(&self, request: TransactionRequest) -> RpcResult<TxHash>;
async fn send_transaction(&self, request: TransactionRequest) -> RpcResult<TxHash>;
}

/// Errors returned by the wallet API.
#[derive(Debug, thiserror::Error)]
pub enum AlphaNetWalletError {
/// 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,
}

impl From<AlphaNetWalletError> for jsonrpsee::types::error::ErrorObject<'static> {
fn from(error: AlphaNetWalletError) -> Self {
jsonrpsee::types::error::ErrorObject::owned::<()>(
jsonrpsee::types::error::INVALID_PARAMS_CODE,
error.to_string(),
None,
)
}
}

/// Implementation of the AlphaNet `wallet_` namespace.
pub struct AlphaNetWallet<Provider, Eth> {
provider: Provider,
wallet: EthereumWallet,
chain_id: ChainId,
capabilities: WalletCapabilities,
eth_api: Eth,
}

impl<Provider, Eth> AlphaNetWallet<Provider, Eth> {
/// Create a new AlphaNet wallet module.
pub fn new(
provider: Provider,
wallet: EthereumWallet,
eth_api: Eth,
chain_id: ChainId,
valid_designations: Vec<Address>,
) -> Self {
let mut caps = HashMap::default();
caps.insert(
chain_id,
Capabilities { delegation: DelegationCapability { addresses: valid_designations } },
);

Self { provider, wallet, eth_api, chain_id, capabilities: WalletCapabilities(caps) }
}
}

#[async_trait]
impl<Provider, Eth> AlphaNetWalletApiServer for AlphaNetWallet<Provider, Eth>
where
Provider: StateProviderFactory + Send + Sync + 'static,
Eth: FullEthApi + Send + Sync + 'static,
{
fn get_capabilities(&self) -> RpcResult<WalletCapabilities> {
trace!(target: "rpc::wallet", "Serving wallet_getCapabilities");
Ok(self.capabilities.clone())
}

async fn send_transaction(&self, mut request: TransactionRequest) -> RpcResult<TxHash> {
trace!(target: "rpc::wallet", ?request, "Serving wallet_sendTransaction");

// validate fields common to eip-7702 and eip-1559
validate_tx_request(&request)?;

let valid_delegations: &[Address] = self
.capabilities
.get(self.chain_id)
.map(|caps| caps.delegation.addresses.as_ref())
.unwrap_or_default();
if let Some(authorizations) = &request.authorization_list {
// check that all auth items delegate to a valid address
if authorizations.iter().any(|auth| !valid_delegations.contains(&auth.address)) {
return Err(AlphaNetWalletError::InvalidAuthorization.into());
}
}

// validate destination
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 state = self.provider.latest().unwrap();
let delegated_address = state
.account_code(addr)
.ok()
.flatten()
.and_then(|code| match code.0 {
Bytecode::Eip7702(code) => Some(code.address()),
_ => None,
})
.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(AlphaNetWalletError::IllegalDestination.into());
}
}
// if it's an eip-7702 tx, let it through
(true, _) => (),
// create tx's disallowed
_ => return Err(AlphaNetWalletError::IllegalDestination.into()),
}

// set nonce
let tx_count = EthState::transaction_count(
&self.eth_api,
NetworkWallet::<Ethereum>::default_signer_address(&self.wallet),
Some(BlockId::pending()),
)
.await
.map_err(Into::into)?;
request.nonce = Some(tx_count.to());

// set chain id
request.chain_id = Some(self.chain_id);

// set gas limit
let estimate =
EthCall::estimate_gas_at(&self.eth_api, request.clone(), BlockId::latest(), None)
.await
.map_err(Into::into)?;
request = request.gas_limit(estimate.to());

// build and sign
let envelope =
<TransactionRequest as TransactionBuilder<Ethereum>>::build::<EthereumWallet>(
request,
&self.wallet,
)
.await
.map_err(|_| AlphaNetWalletError::InvalidTransactionRequest)?;

// this uses the internal `OpEthApi` to either forward the tx to the sequencer, or add it to
// the txpool
//
// see: https://github.com/paradigmxyz/reth/blob/b67f004fbe8e1b7c05f84f314c4c9f2ed9be1891/crates/optimism/rpc/src/eth/transaction.rs#L35-L57
EthTransactions::send_raw_transaction(&self.eth_api, envelope.encoded_2718().into())
.await
.map_err(Into::into)
}
}

fn validate_tx_request(request: &TransactionRequest) -> Result<(), AlphaNetWalletError> {
// reject transactions that have a non-zero value to prevent draining the sequencer.
if request.value.is_some_and(|val| val > U256::ZERO) {
return Err(AlphaNetWalletError::ValueNotZero);
}

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

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

Ok(())
}

0 comments on commit 5b7da3f

Please sign in to comment.