Skip to content
This repository was archived by the owner on Nov 25, 2024. It is now read-only.

Commit 038ed31

Browse files
committed
feat: impl wallet_ namespace
1 parent fa3f2e7 commit 038ed31

File tree

6 files changed

+276
-3
lines changed

6 files changed

+276
-3
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ reth-chainspec = { git = "https://github.com/paradigmxyz/reth.git", rev = "78426
8080
] }
8181
reth-cli = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
8282
reth-cli-util = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
83+
reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
8384
reth-node-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
8485
reth-node-builder = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
8586
reth-node-core = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [
@@ -106,6 +107,7 @@ reth-provider = { git = "https://github.com/paradigmxyz/reth.git", rev = "784267
106107
reth-revm = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673", features = [
107108
"optimism",
108109
] }
110+
reth-storage-api = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
109111
reth-tracing = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
110112
reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth.git", rev = "7842673" }
111113

@@ -119,6 +121,7 @@ tracing = "0.1.0"
119121
serde = "1"
120122
serde_json = "1"
121123
once_cell = "1.19"
124+
thiserror = "1"
122125

123126
# misc-testing
124127
rstest = "0.18.2"

bin/alphanet/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ default-run = "alphanet"
1212
workspace = true
1313

1414
[dependencies]
15+
alloy-signer-local.workspace = true
16+
alloy-network.workspace = true
17+
alloy-primitives.workspace = true
1518
alphanet-node.workspace = true
19+
alphanet-wallet.workspace = true
20+
eyre.workspace = true
1621
tracing.workspace = true
1722
reth-cli-util.workspace = true
1823
reth-node-builder.workspace = true

bin/alphanet/src/main.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,19 @@
2323
//! - `min-debug-logs`: Disables all logs below `debug` level.
2424
//! - `min-trace-logs`: Disables all logs below `trace` level.
2525
26+
use alloy_network::EthereumWallet;
27+
use alloy_primitives::Address;
28+
use alloy_signer_local::PrivateKeySigner;
2629
use alphanet_node::{chainspec::AlphanetChainSpecParser, node::AlphaNetNode};
30+
use alphanet_wallet::{AlphaNetWallet, AlphaNetWalletApiServer};
2731
use clap::Parser;
32+
use eyre::Context;
2833
use reth_node_builder::{engine_tree_config::TreeConfig, EngineNodeLauncher};
2934
use reth_optimism_cli::Cli;
3035
use reth_optimism_node::{args::RollupArgs, node::OptimismAddOns};
3136
use reth_optimism_rpc::sequencer::SequencerClient;
3237
use reth_provider::providers::BlockchainProvider2;
38+
use tracing::{info, warn};
3339

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

69+
// register alphanet wallet namespace
70+
if let Ok(sk) = std::env::var("EXP1_SK") {
71+
let signer: PrivateKeySigner =
72+
sk.parse().wrap_err("Invalid EXP0001 secret key.")?;
73+
let wallet = EthereumWallet::from(signer);
74+
75+
let raw_delegations = std::env::var("EXP1_WHITELIST")
76+
.wrap_err("No EXP0001 delegations specified")?;
77+
let valid_delegations: Vec<Address> = raw_delegations
78+
.split(',')
79+
.map(|addr| Address::parse_checksummed(addr, None))
80+
.collect::<Result<_, _>>()
81+
.wrap_err("No valid EXP0001 delegations specified")?;
82+
83+
ctx.modules.merge_configured(
84+
AlphaNetWallet::new(
85+
ctx.provider().clone(),
86+
wallet,
87+
ctx.registry.eth_api().clone(),
88+
ctx.config().chain.chain().id(),
89+
valid_delegations,
90+
)
91+
.into_rpc(),
92+
)?;
93+
94+
info!(target: "reth::cli", "EXP0001 wallet configured");
95+
} else {
96+
warn!(target: "reth::cli", "EXP0001 wallet not configured");
97+
}
98+
6399
Ok(())
64100
})
65101
.launch_with_fn(|builder| {

crates/wallet/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ keywords.workspace = true
1010
categories.workspace = true
1111

1212
[dependencies]
13+
alloy-network.workspace = true
1314
alloy-primitives.workspace = true
1415
alloy-rpc-types.workspace = true
1516
jsonrpsee = { workspace = true, features = ["server", "macros"] }
17+
reth-primitives.workspace = true
18+
reth-storage-api.workspace = true
19+
reth-rpc-eth-api.workspace = true
1620
serde = { workspace = true, features = ["derive"] }
21+
thiserror.workspace = true
22+
tracing.workspace = true
1723

1824
[lints]
1925
workspace = true

crates/wallet/src/lib.rs

Lines changed: 215 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,20 @@
1717
1818
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
1919

20-
use alloy_primitives::{map::HashMap, Address, ChainId, TxHash};
20+
use alloy_network::{
21+
eip2718::Encodable2718, Ethereum, EthereumWallet, NetworkWallet, TransactionBuilder,
22+
};
23+
use alloy_primitives::{map::HashMap, Address, ChainId, TxHash, TxKind, U256};
2124
use alloy_rpc_types::TransactionRequest;
22-
use jsonrpsee::{core::RpcResult, proc_macros::rpc};
25+
use jsonrpsee::{
26+
core::{async_trait, RpcResult},
27+
proc_macros::rpc,
28+
};
29+
use reth_primitives::{revm_primitives::Bytecode, BlockId};
30+
use reth_rpc_eth_api::helpers::{EthCall, EthState, EthTransactions, FullEthApi};
31+
use reth_storage_api::{StateProvider, StateProviderFactory};
2332
use serde::{Deserialize, Serialize};
33+
use tracing::trace;
2434

2535
/// The capability to perform [EIP-7702][eip-7702] delegations, sponsored by the sequencer.
2636
///
@@ -45,6 +55,13 @@ pub struct Capabilities {
4555
#[derive(Debug, Clone, Deserialize, Serialize)]
4656
pub struct WalletCapabilities(pub HashMap<ChainId, Capabilities>);
4757

58+
impl WalletCapabilities {
59+
/// Get the capabilities of the wallet API for the specified chain ID.
60+
pub fn get(&self, chain_id: ChainId) -> Option<&Capabilities> {
61+
self.0.get(&chain_id)
62+
}
63+
}
64+
4865
/// AlphaNet `wallet_` RPC namespace.
4966
#[cfg_attr(not(test), rpc(server, namespace = "wallet"))]
5067
#[cfg_attr(test, rpc(server, client, namespace = "wallet"))]
@@ -75,5 +92,200 @@ pub trait AlphaNetWalletApi {
7592
/// [eip-7702]: https://eips.ethereum.org/EIPS/eip-7702
7693
/// [eip-1559]: https://eips.ethereum.org/EIPS/eip-1559
7794
#[method(name = "sendTransaction")]
78-
fn send_transaction(&self, request: TransactionRequest) -> RpcResult<TxHash>;
95+
async fn send_transaction(&self, request: TransactionRequest) -> RpcResult<TxHash>;
96+
}
97+
98+
/// Errors returned by the wallet API.
99+
#[derive(Debug, thiserror::Error)]
100+
pub enum AlphaNetWalletError {
101+
/// The transaction value is not 0.
102+
///
103+
/// The value should be 0 to prevent draining the sequencer.
104+
#[error("tx value not zero")]
105+
ValueNotZero,
106+
/// The from field is set on the transaction.
107+
///
108+
/// Requests with the from field are rejected, since it is implied that it will always be the
109+
/// sequencer.
110+
#[error("tx from field is set")]
111+
FromSet,
112+
/// The nonce field is set on the transaction.
113+
///
114+
/// Requests with the nonce field set are rejected, as this is managed by the sequencer.
115+
#[error("tx nonce is set")]
116+
NonceSet,
117+
/// An authorization item was invalid.
118+
///
119+
/// The item is invalid if it tries to delegate an account to a contract that is not
120+
/// whitelisted.
121+
#[error("invalid authorization address")]
122+
InvalidAuthorization,
123+
/// The to field of the transaction was invalid.
124+
///
125+
/// The destination is invalid if:
126+
///
127+
/// - There is no bytecode at the destination, or
128+
/// - The bytecode is not an EIP-7702 delegation designator, or
129+
/// - The delegation designator points to a contract that is not whitelisted
130+
#[error("the destination of the transaction is not a delegated account")]
131+
IllegalDestination,
132+
/// The transaction request was invalid.
133+
///
134+
/// This is likely an internal error, as most of the request is built by the sequencer.
135+
#[error("invalid tx request")]
136+
InvalidTransactionRequest,
137+
}
138+
139+
impl From<AlphaNetWalletError> for jsonrpsee::types::error::ErrorObject<'static> {
140+
fn from(error: AlphaNetWalletError) -> Self {
141+
jsonrpsee::types::error::ErrorObject::owned::<()>(
142+
jsonrpsee::types::error::INVALID_PARAMS_CODE,
143+
error.to_string(),
144+
None,
145+
)
146+
}
147+
}
148+
149+
/// Implementation of the AlphaNet `wallet_` namespace.
150+
pub struct AlphaNetWallet<Provider, Eth> {
151+
provider: Provider,
152+
wallet: EthereumWallet,
153+
chain_id: ChainId,
154+
capabilities: WalletCapabilities,
155+
eth_api: Eth,
156+
}
157+
158+
impl<Provider, Eth> AlphaNetWallet<Provider, Eth> {
159+
/// Create a new AlphaNet wallet module.
160+
pub fn new(
161+
provider: Provider,
162+
wallet: EthereumWallet,
163+
eth_api: Eth,
164+
chain_id: ChainId,
165+
valid_designations: Vec<Address>,
166+
) -> Self {
167+
let mut caps = HashMap::default();
168+
caps.insert(
169+
chain_id,
170+
Capabilities { delegation: DelegationCapability { addresses: valid_designations } },
171+
);
172+
173+
Self { provider, wallet, eth_api, chain_id, capabilities: WalletCapabilities(caps) }
174+
}
175+
}
176+
177+
#[async_trait]
178+
impl<Provider, Eth> AlphaNetWalletApiServer for AlphaNetWallet<Provider, Eth>
179+
where
180+
Provider: StateProviderFactory + Send + Sync + 'static,
181+
Eth: FullEthApi + Send + Sync + 'static,
182+
{
183+
fn get_capabilities(&self) -> RpcResult<WalletCapabilities> {
184+
trace!(target: "rpc::wallet", "Serving wallet_getCapabilities");
185+
Ok(self.capabilities.clone())
186+
}
187+
188+
async fn send_transaction(&self, mut request: TransactionRequest) -> RpcResult<TxHash> {
189+
trace!(target: "rpc::wallet", ?request, "Serving wallet_sendTransaction");
190+
191+
// validate fields common to eip-7702 and eip-1559
192+
validate_tx_request(&request)?;
193+
194+
let valid_delegations: &[Address] = self
195+
.capabilities
196+
.get(self.chain_id)
197+
.map(|caps| caps.delegation.addresses.as_ref())
198+
.unwrap_or_default();
199+
if let Some(authorizations) = &request.authorization_list {
200+
// check that all auth items delegate to a valid address
201+
if authorizations.iter().any(|auth| !valid_delegations.contains(&auth.address)) {
202+
return Err(AlphaNetWalletError::InvalidAuthorization.into());
203+
}
204+
}
205+
206+
// validate destination
207+
match (request.authorization_list.is_some(), request.to) {
208+
// if this is an eip-1559 tx, ensure that it is an account that delegates to a
209+
// whitelisted address
210+
(false, Some(TxKind::Call(addr))) => {
211+
let state = self.provider.latest().unwrap();
212+
let delegated_address = state
213+
.account_code(addr)
214+
.ok()
215+
.flatten()
216+
.and_then(|code| match code.0 {
217+
Bytecode::Eip7702(code) => Some(code.address()),
218+
_ => None,
219+
})
220+
.unwrap_or_default();
221+
222+
// not a whitelisted address, or not an eip-7702 bytecode
223+
if delegated_address == Address::ZERO
224+
|| !valid_delegations.contains(&delegated_address)
225+
{
226+
return Err(AlphaNetWalletError::IllegalDestination.into());
227+
}
228+
}
229+
// if it's an eip-7702 tx, let it through
230+
(true, _) => (),
231+
// create tx's disallowed
232+
_ => return Err(AlphaNetWalletError::IllegalDestination.into()),
233+
}
234+
235+
// set nonce
236+
let tx_count = EthState::transaction_count(
237+
&self.eth_api,
238+
NetworkWallet::<Ethereum>::default_signer_address(&self.wallet),
239+
Some(BlockId::pending()),
240+
)
241+
.await
242+
.map_err(Into::into)?;
243+
request.nonce = Some(tx_count.to());
244+
245+
// set chain id
246+
request.chain_id = Some(self.chain_id);
247+
248+
// set gas limit
249+
let estimate =
250+
EthCall::estimate_gas_at(&self.eth_api, request.clone(), BlockId::latest(), None)
251+
.await
252+
.map_err(Into::into)?;
253+
request = request.gas_limit(estimate.to());
254+
255+
// build and sign
256+
let envelope =
257+
<TransactionRequest as TransactionBuilder<Ethereum>>::build::<EthereumWallet>(
258+
request,
259+
&self.wallet,
260+
)
261+
.await
262+
.map_err(|_| AlphaNetWalletError::InvalidTransactionRequest)?;
263+
264+
// this uses the internal `OpEthApi` to either forward the tx to the sequencer, or add it to
265+
// the txpool
266+
//
267+
// see: https://github.com/paradigmxyz/reth/blob/b67f004fbe8e1b7c05f84f314c4c9f2ed9be1891/crates/optimism/rpc/src/eth/transaction.rs#L35-L57
268+
EthTransactions::send_raw_transaction(&self.eth_api, envelope.encoded_2718().into())
269+
.await
270+
.map_err(Into::into)
271+
}
272+
}
273+
274+
fn validate_tx_request(request: &TransactionRequest) -> Result<(), AlphaNetWalletError> {
275+
// reject transactions that have a non-zero value to prevent draining the sequencer.
276+
if request.value.is_some_and(|val| val > U256::ZERO) {
277+
return Err(AlphaNetWalletError::ValueNotZero.into());
278+
}
279+
280+
// reject transactions that have from set, as this will be the sequencer.
281+
if request.from.is_some() {
282+
return Err(AlphaNetWalletError::FromSet.into());
283+
}
284+
285+
// reject transaction requests that have nonce set, as this is managed by the sequencer.
286+
if request.nonce.is_some() {
287+
return Err(AlphaNetWalletError::NonceSet.into());
288+
}
289+
290+
Ok(())
79291
}

0 commit comments

Comments
 (0)