Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 38 additions & 79 deletions crates/ethrpc/src/alloy/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ use {
alloy_network::{Ethereum, EthereumWallet, Network, NetworkWallet, TxSigner},
alloy_primitives::Address,
alloy_signer::Signature,
alloy_transport::impl_future,
std::{sync::Arc, thread},
tokio::sync::RwLock,
std::{
ops::Deref,
sync::{Arc, RwLock},
},
};

/// A mutable version of [`EthereumWallet`], cheaply cloneable (through
/// [`Arc`]).
///
/// Requires a tokio runtime to be present, otherwise operations will panic.
// We also wrap the inner [`EthereumWallet`] in an [`Arc`] because
// we don't want to deep clone the entire thing every time we need to
// sign something.
#[derive(Debug, Clone, Default)]
pub struct MutWallet(Arc<RwLock<EthereumWallet>>);
pub struct MutWallet(Arc<RwLock<Arc<EthereumWallet>>>);

impl MutWallet {
pub fn new(wallet: EthereumWallet) -> Self {
Self(Arc::new(RwLock::new(wallet)))
Self(Arc::new(RwLock::new(Arc::new(wallet))))
}
}

impl MutWallet {
/// Calls the inner [`EthereumWallet`]'s
/// [`register_signer`](EthereumWallet::register_signer), if no default
/// signer has been setup (i.e. the wallet was created using
Expand All @@ -30,57 +30,24 @@ impl MutWallet {
where
S: TxSigner<Signature> + Send + Sync + 'static,
{
self.handle_blocking_operation(move |wallet| {
// If the wallet is created using MutWallet::default(), there will not be
// default signer; this stops us from *not* using `.from` (since it
// is filled with the default signer). At the same time, we can't
// constantly register new default signers, because it breaks the caller's
// expectations. As such, if the current default signer address is
// the default address (0x000...000) we register the signer as the
// default one.
let register_default = {
let r_lock = wallet.0.blocking_read();
let default_address =
<EthereumWallet as NetworkWallet<Ethereum>>::default_signer_address(&r_lock);

default_address == Address::default()
};

let mut w_lock = wallet.0.blocking_write();
if register_default {
w_lock.register_default_signer(signer);
} else {
w_lock.register_signer(signer);
}
});
}
// If the wallet is created using MutWallet::default(), there will not be
// default signer; this stops us from *not* using `.from` (since it
// is filled with the default signer). At the same time, we can't
// constantly register new default signers, because it breaks the caller's
// expectations. As such, if the current default signer address is
// the default address (0x000...000) we register the signer as the
// default one.
let mut w_lock = self.0.write().unwrap();
let default_address =
<EthereumWallet as NetworkWallet<Ethereum>>::default_signer_address(&w_lock);

/// Handles blocking operations such as the
/// [`blocking_read`](RwLock::blocking_read)
/// and [`blocking_write`](RwLock::blocking_write).
///
/// This function *will panic* in case there is no runtime present, or the
/// runtime flavor is not `current_thread` or `multi_thread`.
// This function is necessary to handle the blocking lock operations under
// required by synchronous function calls which are problematic when the runtime
// flavour is `current_thread` (which will panic when blocked by certain
// operations).
fn handle_blocking_operation<F, R>(&self, f: F) -> R
where
F: FnOnce(Self) -> R + Send + 'static,
R: Send + 'static,
{
let wallet = self.clone();
let rt = tokio::runtime::Handle::current();

match rt.runtime_flavor() {
tokio::runtime::RuntimeFlavor::CurrentThread => thread::spawn(move || f(wallet))
.join()
.expect("failed to join thread"),
tokio::runtime::RuntimeFlavor::MultiThread => {
tokio::task::block_in_place(move || f(wallet))
}
_ => panic!("unsupported runtime flavor"),
// note that [`Arc::make_mut()`] will never perform a deep clone because
// we never give out clones of the inner `Arc` and we take a
// write lock in this function which gives us exclusive access.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claim "we never give out clones of the inner Arc" isn't strictly accurate anymore: sign_transaction_from does Arc::clone(self.0.read().unwrap().deref()) (line 89). So if a sign_transaction_from future is in flight while register_signer runs, the strong count is >1 and Arc::make_mut will deep-clone here.

That's harmless for correctness (make_mut deep-clones precisely so the mutation stays isolated), but the comment's absolute wording is misleading. Per the PR description all signers are registered up-front before any signing, so this doesn't happen in practice — worth softening the comment to say the deep clone only happens if a sign is concurrently in flight, which shouldn't occur given the registration-at-startup usage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, true. Because you can't extract the inner wallet from the returned future the worst that could happen is a deep clone in register_signers but we can't run into the scenario where we have multiple MutWallet instances flying around that have different sets of signers. That would be a real headache to debug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude check that the updated comment is fully accurate.

if default_address.is_zero() {
Arc::make_mut(&mut w_lock).register_default_signer(signer);
} else {
Arc::make_mut(&mut w_lock).register_signer(signer);
}
}
}
Expand All @@ -93,41 +60,33 @@ where
/// in [`NetworkWallet::sign_transaction_from`] when no specific signer is
/// specified.
fn default_signer_address(&self) -> Address {
self.handle_blocking_operation(|wallet| {
let r_lock = wallet.0.blocking_read();
<EthereumWallet as NetworkWallet<N>>::default_signer_address(&r_lock)
})
let r_lock = self.0.read().unwrap();
<EthereumWallet as NetworkWallet<N>>::default_signer_address(&r_lock)
}

/// Return true if the signer contains a credential for the given address.
fn has_signer_for(&self, address: &Address) -> bool {
let address = *address;
self.handle_blocking_operation(move |wallet| {
let r_lock = wallet.0.blocking_read();
<EthereumWallet as NetworkWallet<N>>::has_signer_for(&r_lock, &address)
})
let r_lock = self.0.read().unwrap();
<EthereumWallet as NetworkWallet<N>>::has_signer_for(&r_lock, address)
}

/// Return an iterator of all signer addresses.
fn signer_addresses(&self) -> impl Iterator<Item = Address> {
self.handle_blocking_operation(move |wallet| {
let r_lock = wallet.0.blocking_read();
<EthereumWallet as NetworkWallet<N>>::signer_addresses(&r_lock).collect::<Vec<_>>()
})
.into_iter()
let r_lock = self.0.read().unwrap();
<EthereumWallet as NetworkWallet<N>>::signer_addresses(&r_lock)
.collect::<Vec<_>>()
.into_iter()
}

/// Asynchronously sign an unsigned transaction, with a specified
/// credential.
#[doc(alias = "sign_tx_from")]
fn sign_transaction_from(
async fn sign_transaction_from(
&self,
sender: Address,
tx: N::UnsignedTx,
) -> impl_future!(<Output = alloy_signer::Result<N::TxEnvelope>>) {
async move {
let r_lock = self.0.read().await;
<EthereumWallet as NetworkWallet<N>>::sign_transaction_from(&r_lock, sender, tx).await
}
) -> alloy_signer::Result<N::TxEnvelope> {
let wallet = Arc::clone(self.0.read().unwrap().deref());
<EthereumWallet as NetworkWallet<N>>::sign_transaction_from(&wallet, sender, tx).await
}
}
Loading