feat: derive dev accounts from mnemonic in dev mode (#18299)

Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
This commit is contained in:
Dharm Singh
2025-10-17 20:49:21 +05:30
committed by GitHub
parent 928d91dbf9
commit 1b830e9ed1
11 changed files with 126 additions and 81 deletions

View File

@@ -23,8 +23,8 @@ use reth_node_core::{
version::{version_metadata, CLIENT_CODE},
};
use reth_payload_builder::{PayloadBuilderHandle, PayloadStore};
use reth_rpc::eth::{core::EthRpcConverterFor, EthApiTypes, FullEthApiServer};
use reth_rpc_api::{eth::helpers::AddDevSigners, IntoEngineApiRpcModule};
use reth_rpc::eth::{core::EthRpcConverterFor, DevSigner, EthApiTypes, FullEthApiServer};
use reth_rpc_api::{eth::helpers::EthTransactions, IntoEngineApiRpcModule};
use reth_rpc_builder::{
auth::{AuthRpcModule, AuthServerHandle},
config::RethRpcServerConfig,
@@ -991,7 +991,8 @@ where
// in dev mode we generate 20 random dev-signer accounts
if config.dev.dev {
registry.eth_api().with_dev_accounts();
let signers = DevSigner::from_mnemonic(config.dev.dev_mnemonic.as_str(), 20);
registry.eth_api().signers().write().extend(signers);
}
let mut registry = RpcRegistry { registry };
@@ -1163,7 +1164,6 @@ pub trait EthApiBuilder<N: FullNodeComponents>: Default + Send + 'static {
/// The Ethapi implementation this builder will build.
type EthApi: EthApiTypes
+ FullEthApiServer<Provider = N::Provider, Pool = N::Pool>
+ AddDevSigners
+ Unpin
+ 'static;

View File

@@ -5,8 +5,10 @@ use std::time::Duration;
use clap::Args;
use humantime::parse_duration;
const DEFAULT_MNEMONIC: &str = "test test test test test test test test test test test junk";
/// Parameters for Dev testnet configuration
#[derive(Debug, Args, PartialEq, Eq, Default, Clone, Copy)]
#[derive(Debug, Args, PartialEq, Eq, Clone)]
#[command(next_help_heading = "Dev testnet")]
pub struct DevArgs {
/// Start the node in dev mode
@@ -39,6 +41,28 @@ pub struct DevArgs {
verbatim_doc_comment
)]
pub block_time: Option<Duration>,
/// Derive dev accounts from a fixed mnemonic instead of random ones.
#[arg(
long = "dev.mnemonic",
help_heading = "Dev testnet",
value_name = "MNEMONIC",
requires = "dev",
verbatim_doc_comment,
default_value = DEFAULT_MNEMONIC
)]
pub dev_mnemonic: String,
}
impl Default for DevArgs {
fn default() -> Self {
Self {
dev: false,
block_max_transactions: None,
block_time: None,
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
}
}
#[cfg(test)]
@@ -56,13 +80,37 @@ mod tests {
#[test]
fn test_parse_dev_args() {
let args = CommandParser::<DevArgs>::parse_from(["reth"]).args;
assert_eq!(args, DevArgs { dev: false, block_max_transactions: None, block_time: None });
assert_eq!(
args,
DevArgs {
dev: false,
block_max_transactions: None,
block_time: None,
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
);
let args = CommandParser::<DevArgs>::parse_from(["reth", "--dev"]).args;
assert_eq!(args, DevArgs { dev: true, block_max_transactions: None, block_time: None });
assert_eq!(
args,
DevArgs {
dev: true,
block_max_transactions: None,
block_time: None,
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
);
let args = CommandParser::<DevArgs>::parse_from(["reth", "--auto-mine"]).args;
assert_eq!(args, DevArgs { dev: true, block_max_transactions: None, block_time: None });
assert_eq!(
args,
DevArgs {
dev: true,
block_max_transactions: None,
block_time: None,
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
);
let args = CommandParser::<DevArgs>::parse_from([
"reth",
@@ -71,7 +119,15 @@ mod tests {
"2",
])
.args;
assert_eq!(args, DevArgs { dev: true, block_max_transactions: Some(2), block_time: None });
assert_eq!(
args,
DevArgs {
dev: true,
block_max_transactions: Some(2),
block_time: None,
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
);
let args =
CommandParser::<DevArgs>::parse_from(["reth", "--dev", "--dev.block-time", "1s"]).args;
@@ -80,7 +136,8 @@ mod tests {
DevArgs {
dev: true,
block_max_transactions: None,
block_time: Some(std::time::Duration::from_secs(1))
block_time: Some(std::time::Duration::from_secs(1)),
dev_mnemonic: DEFAULT_MNEMONIC.to_string(),
}
);
}

View File

@@ -272,7 +272,7 @@ impl<ChainSpec> NodeConfig<ChainSpec> {
}
/// Set the dev args for the node
pub const fn with_dev(mut self, dev: DevArgs) -> Self {
pub fn with_dev(mut self, dev: DevArgs) -> Self {
self.dev = dev;
self
}
@@ -519,7 +519,7 @@ impl<ChainSpec> Clone for NodeConfig<ChainSpec> {
builder: self.builder.clone(),
debug: self.debug.clone(),
db: self.db,
dev: self.dev,
dev: self.dev.clone(),
pruning: self.pruning.clone(),
datadir: self.datadir.clone(),
engine: self.engine.clone(),

View File

@@ -26,19 +26,19 @@ use reth_optimism_flashblocks::{
ExecutionPayloadBaseV1, FlashBlockBuildInfo, FlashBlockCompleteSequenceRx, FlashBlockService,
InProgressFlashBlockRx, PendingBlockRx, PendingFlashBlock, WsFlashBlockStream,
};
use reth_rpc::eth::{core::EthApiInner, DevSigner};
use reth_rpc::eth::core::EthApiInner;
use reth_rpc_eth_api::{
helpers::{
pending_block::BuildPendingEnv, AddDevSigners, EthApiSpec, EthFees, EthState, LoadFee,
LoadPendingBlock, LoadState, SpawnBlocking, Trace,
pending_block::BuildPendingEnv, EthApiSpec, EthFees, EthState, LoadFee, LoadPendingBlock,
LoadState, SpawnBlocking, Trace,
},
EthApiTypes, FromEvmError, FullEthApiServer, RpcConvert, RpcConverter, RpcNodeCore,
RpcNodeCoreExt, RpcTypes, SignableTxRequest,
RpcNodeCoreExt, RpcTypes,
};
use reth_rpc_eth_types::{
EthStateCache, FeeHistoryCache, GasPriceOracle, PendingBlock, PendingBlockEnvOrigin,
};
use reth_storage_api::{ProviderHeader, ProviderTx};
use reth_storage_api::ProviderHeader;
use reth_tasks::{
pool::{BlockingTaskGuard, BlockingTaskPool},
TaskSpawner,
@@ -335,18 +335,6 @@ where
{
}
impl<N, Rpc> AddDevSigners for OpEthApi<N, Rpc>
where
N: RpcNodeCore,
Rpc: RpcConvert<
Network: RpcTypes<TransactionRequest: SignableTxRequest<ProviderTx<N::Provider>>>,
>,
{
fn with_dev_accounts(&self) {
*self.inner.eth_api.signers().write() = DevSigner::random_signers(20)
}
}
impl<N: RpcNodeCore, Rpc: RpcConvert> fmt::Debug for OpEthApi<N, Rpc> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OpEthApi").finish_non_exhaustive()
@@ -483,7 +471,7 @@ where
NetworkT: RpcTypes,
OpRpcConvert<N, NetworkT>: RpcConvert<Network = NetworkT>,
OpEthApi<N, OpRpcConvert<N, NetworkT>>:
FullEthApiServer<Provider = N::Provider, Pool = N::Pool> + AddDevSigners,
FullEthApiServer<Provider = N::Provider, Pool = N::Pool>,
{
type EthApi = OpEthApi<N, OpRpcConvert<N, NetworkT>>;

View File

@@ -2,7 +2,7 @@
use crate::{
fees::{CallFees, CallFeesError},
RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes,
RpcHeader, RpcReceipt, RpcTransaction, RpcTxReq, RpcTypes, SignableTxRequest,
};
use alloy_consensus::{
error::ValueError, transaction::Recovered, EthereumTxEnvelope, Sealable, TxEip4844,
@@ -128,7 +128,7 @@ pub trait RpcConvert: Send + Sync + Unpin + Debug + DynClone + 'static {
/// Associated upper layer JSON-RPC API network requests and responses to convert from and into
/// types of [`Self::Primitives`].
type Network: RpcTypes + Send + Sync + Unpin + Clone + Debug;
type Network: RpcTypes<TransactionRequest: SignableTxRequest<TxTy<Self::Primitives>>>;
/// An associated RPC conversion error.
type Error: error::Error + Into<jsonrpsee_types::ErrorObject<'static>>;
@@ -901,7 +901,7 @@ impl<N, Network, Evm, Receipt, Header, Map, SimTx, RpcTx, TxEnv> RpcConvert
for RpcConverter<Network, Evm, Receipt, Header, Map, SimTx, RpcTx, TxEnv>
where
N: NodePrimitives,
Network: RpcTypes + Send + Sync + Unpin + Clone + Debug,
Network: RpcTypes<TransactionRequest: SignableTxRequest<N::SignedTx>>,
Evm: ConfigureEvm<Primitives = N> + 'static,
Receipt: ReceiptConverter<
N,

View File

@@ -34,7 +34,7 @@ pub use call::{Call, EthCall};
pub use fee::{EthFees, LoadFee};
pub use pending_block::LoadPendingBlock;
pub use receipt::LoadReceipt;
pub use signer::{AddDevSigners, EthSigner};
pub use signer::EthSigner;
pub use spec::EthApiSpec;
pub use state::{EthState, LoadState};
pub use trace::Trace;

View File

@@ -32,11 +32,3 @@ pub trait EthSigner<T, TxReq = TransactionRequest>: Send + Sync + DynClone {
}
dyn_clone::clone_trait_object!(<T> EthSigner<T>);
/// Adds 20 random dev signers for access via the API. Used in dev mode.
#[auto_impl::auto_impl(&)]
pub trait AddDevSigners {
/// Generates 20 random developer accounts.
/// Used in DEV mode.
fn with_dev_accounts(&self);
}

View File

@@ -2,11 +2,9 @@
use crate::{AsEthApiError, FromEthApiError, RpcNodeCore};
use alloy_rpc_types_eth::Block;
use reth_chain_state::CanonStateSubscriptions;
use reth_rpc_convert::RpcConvert;
use reth_rpc_convert::{RpcConvert, SignableTxRequest};
pub use reth_rpc_convert::{RpcTransaction, RpcTxReq, RpcTypes};
use reth_storage_api::{ProviderTx, ReceiptProvider, TransactionsProvider};
use reth_transaction_pool::{PoolTransaction, TransactionPool};
use reth_storage_api::ProviderTx;
use std::{
error::Error,
fmt::{self},
@@ -52,12 +50,11 @@ pub type RpcError<T> = <T as EthApiTypes>::Error;
/// Helper trait holds necessary trait bounds on [`EthApiTypes`] to implement `eth` API.
pub trait FullEthApiTypes
where
Self: RpcNodeCore<
Provider: TransactionsProvider + ReceiptProvider + CanonStateSubscriptions,
Pool: TransactionPool<
Transaction: PoolTransaction<Consensus = ProviderTx<Self::Provider>>,
Self: RpcNodeCore
+ EthApiTypes<
NetworkTypes: RpcTypes<
TransactionRequest: SignableTxRequest<ProviderTx<Self::Provider>>,
>,
> + EthApiTypes<
RpcConvert: RpcConvert<
Primitives = Self::Primitives,
Network = Self::NetworkTypes,
@@ -68,12 +65,11 @@ where
}
impl<T> FullEthApiTypes for T where
T: RpcNodeCore<
Provider: TransactionsProvider + ReceiptProvider + CanonStateSubscriptions,
Pool: TransactionPool<
Transaction: PoolTransaction<Consensus = ProviderTx<Self::Provider>>,
T: RpcNodeCore
+ EthApiTypes<
NetworkTypes: RpcTypes<
TransactionRequest: SignableTxRequest<ProviderTx<Self::Provider>>,
>,
> + EthApiTypes<
RpcConvert: RpcConvert<
Primitives = <Self as RpcNodeCore>::Primitives,
Network = Self::NetworkTypes,

View File

@@ -45,7 +45,7 @@ reth-trie-common.workspace = true
alloy-evm = { workspace = true, features = ["overrides"] }
alloy-consensus.workspace = true
alloy-signer.workspace = true
alloy-signer-local.workspace = true
alloy-signer-local = { workspace = true, features = ["mnemonic"] }
alloy-eips = { workspace = true, features = ["kzg"] }
alloy-dyn-abi.workspace = true
alloy-genesis.workspace = true

View File

@@ -1,33 +1,14 @@
//! An abstraction over ethereum signers.
use std::collections::HashMap;
use crate::EthApi;
use alloy_dyn_abi::TypedData;
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::{eip191_hash_message, Address, Signature, B256};
use alloy_signer::SignerSync;
use alloy_signer_local::PrivateKeySigner;
use reth_rpc_convert::{RpcConvert, RpcTypes, SignableTxRequest};
use reth_rpc_eth_api::{
helpers::{signer::Result, AddDevSigners, EthSigner},
FromEvmError, RpcNodeCore,
};
use reth_rpc_eth_types::{EthApiError, SignError};
use reth_storage_api::ProviderTx;
impl<N, Rpc> AddDevSigners for EthApi<N, Rpc>
where
N: RpcNodeCore,
EthApiError: FromEvmError<N::Evm>,
Rpc: RpcConvert<
Network: RpcTypes<TransactionRequest: SignableTxRequest<ProviderTx<N::Provider>>>,
>,
{
fn with_dev_accounts(&self) {
*self.inner.signers().write() = DevSigner::random_signers(20)
}
}
use alloy_signer_local::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner};
use reth_rpc_convert::SignableTxRequest;
use reth_rpc_eth_api::helpers::{signer::Result, EthSigner};
use reth_rpc_eth_types::SignError;
use std::collections::HashMap;
/// Holds developer keys
#[derive(Debug, Clone)]
@@ -55,6 +36,32 @@ impl DevSigner {
signers
}
/// Generates dev signers deterministically from a fixed mnemonic.
/// Uses the Ethereum derivation path: `m/44'/60'/0'/0/{index}`
pub fn from_mnemonic<T: Decodable2718, TxReq: SignableTxRequest<T>>(
mnemonic: &str,
num: u32,
) -> Vec<Box<dyn EthSigner<T, TxReq> + 'static>> {
let mut signers = Vec::with_capacity(num as usize);
for i in 0..num {
let sk = MnemonicBuilder::<English>::default()
.phrase(mnemonic)
.index(i)
.expect("invalid derivation path")
.build()
.expect("failed to build signer from mnemonic");
let address = sk.address();
let addresses = vec![address];
let accounts = HashMap::from([(address, sk)]);
signers.push(Box::new(Self { addresses, accounts }) as Box<dyn EthSigner<T, TxReq>>);
}
signers
}
fn get_key(&self, account: Address) -> Result<&PrivateKeySigner> {
self.accounts.get(&account).ok_or(SignError::NoAccount)
}

View File

@@ -734,6 +734,11 @@ Dev testnet:
Parses strings using [`humantime::parse_duration`]
--dev.block-time 12s
--dev.mnemonic <MNEMONIC>
Derive dev accounts from a fixed mnemonic instead of random ones.
[default: "test test test test test test test test test test test junk"]
Pruning:
--full
Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored