mirror of
https://github.com/hl-archive-node/nanoreth.git
synced 2025-12-06 10:59:55 +00:00
Compare commits
4 Commits
db10c23c56
...
nb-2025091
| Author | SHA1 | Date | |
|---|---|---|---|
| 491e902904 | |||
| 45648a7a98 | |||
| c87c5a055a | |||
| c9416a3948 |
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -9350,6 +9350,7 @@ dependencies = [
|
|||||||
"reth-db-api",
|
"reth-db-api",
|
||||||
"reth-discv4",
|
"reth-discv4",
|
||||||
"reth-engine-primitives",
|
"reth-engine-primitives",
|
||||||
|
"reth-errors",
|
||||||
"reth-eth-wire",
|
"reth-eth-wire",
|
||||||
"reth-eth-wire-types",
|
"reth-eth-wire-types",
|
||||||
"reth-ethereum-forks",
|
"reth-ethereum-forks",
|
||||||
@ -9370,9 +9371,13 @@ dependencies = [
|
|||||||
"reth-provider",
|
"reth-provider",
|
||||||
"reth-revm",
|
"reth-revm",
|
||||||
"reth-rpc",
|
"reth-rpc",
|
||||||
|
"reth-rpc-convert",
|
||||||
"reth-rpc-engine-api",
|
"reth-rpc-engine-api",
|
||||||
"reth-rpc-eth-api",
|
"reth-rpc-eth-api",
|
||||||
|
"reth-rpc-eth-types",
|
||||||
|
"reth-rpc-server-types",
|
||||||
"reth-stages-types",
|
"reth-stages-types",
|
||||||
|
"reth-storage-api",
|
||||||
"reth-tracing",
|
"reth-tracing",
|
||||||
"reth-transaction-pool",
|
"reth-transaction-pool",
|
||||||
"reth-trie-common",
|
"reth-trie-common",
|
||||||
|
|||||||
@ -63,6 +63,11 @@ reth-trie-db = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d6
|
|||||||
reth-codecs = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
reth-codecs = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
reth-transaction-pool = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
reth-transaction-pool = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
reth-stages-types = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
reth-stages-types = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
|
reth-storage-api = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
|
reth-errors = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
|
reth-rpc-convert = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
|
reth-rpc-eth-types = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
|
reth-rpc-server-types = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
reth-metrics = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
reth-metrics = { git = "https://github.com/sprites0/reth", rev = "d26fd2e25b57d695aa453c93f15a8cd158a1f505" }
|
||||||
revm = { version = "29.0.0", default-features = false }
|
revm = { version = "29.0.0", default-features = false }
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
|
use core::fmt;
|
||||||
|
|
||||||
use super::{HlEthApi, HlRpcNodeCore};
|
use super::{HlEthApi, HlRpcNodeCore};
|
||||||
use crate::{node::evm::apply_precompiles, HlBlock};
|
use crate::{node::evm::apply_precompiles, HlBlock};
|
||||||
use alloy_evm::Evm;
|
use alloy_evm::Evm;
|
||||||
use alloy_primitives::B256;
|
use alloy_primitives::B256;
|
||||||
use reth::rpc::server_types::eth::EthApiError;
|
use reth::rpc::server_types::eth::EthApiError;
|
||||||
use reth_evm::{ConfigureEvm, Database, EvmEnvFor, SpecFor, TxEnvFor};
|
use reth_evm::{ConfigureEvm, Database, EvmEnvFor, HaltReasonFor, InspectorFor, SpecFor, TxEnvFor};
|
||||||
use reth_primitives::{NodePrimitives, Recovered};
|
use reth_primitives::{NodePrimitives, Recovered};
|
||||||
use reth_primitives_traits::SignedTransaction;
|
use reth_primitives_traits::SignedTransaction;
|
||||||
use reth_provider::{ProviderError, ProviderTx};
|
use reth_provider::{ProviderError, ProviderTx};
|
||||||
use reth_rpc_eth_api::{
|
use reth_rpc_eth_api::{
|
||||||
helpers::{estimate::EstimateCall, Call, EthCall},
|
helpers::{Call, EthCall},
|
||||||
FromEvmError, RpcConvert, RpcNodeCore,
|
FromEvmError, RpcConvert, RpcNodeCore,
|
||||||
};
|
};
|
||||||
use revm::DatabaseCommit;
|
use revm::{context::result::ResultAndState, DatabaseCommit};
|
||||||
|
|
||||||
impl<N> HlRpcNodeCore for N where N: RpcNodeCore<Primitives: NodePrimitives<Block = HlBlock>> {}
|
impl<N> HlRpcNodeCore for N where N: RpcNodeCore<Primitives: NodePrimitives<Block = HlBlock>> {}
|
||||||
|
|
||||||
@ -28,19 +30,6 @@ where
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<N, Rpc> EstimateCall for HlEthApi<N, Rpc>
|
|
||||||
where
|
|
||||||
N: HlRpcNodeCore,
|
|
||||||
EthApiError: FromEvmError<N::Evm>,
|
|
||||||
Rpc: RpcConvert<
|
|
||||||
Primitives = N::Primitives,
|
|
||||||
Error = EthApiError,
|
|
||||||
TxEnv = TxEnvFor<N::Evm>,
|
|
||||||
Spec = SpecFor<N::Evm>,
|
|
||||||
>,
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<N, Rpc> Call for HlEthApi<N, Rpc>
|
impl<N, Rpc> Call for HlEthApi<N, Rpc>
|
||||||
where
|
where
|
||||||
N: HlRpcNodeCore,
|
N: HlRpcNodeCore,
|
||||||
@ -62,6 +51,46 @@ where
|
|||||||
self.inner.eth_api.max_simulate_blocks()
|
self.inner.eth_api.max_simulate_blocks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn transact<DB>(
|
||||||
|
&self,
|
||||||
|
db: DB,
|
||||||
|
evm_env: EvmEnvFor<Self::Evm>,
|
||||||
|
tx_env: TxEnvFor<Self::Evm>,
|
||||||
|
) -> Result<ResultAndState<HaltReasonFor<Self::Evm>>, Self::Error>
|
||||||
|
where
|
||||||
|
DB: Database<Error = ProviderError> + fmt::Debug,
|
||||||
|
{
|
||||||
|
let block_number = evm_env.block_env().number;
|
||||||
|
let hl_extras = self.get_hl_extras(block_number.try_into().unwrap())?;
|
||||||
|
|
||||||
|
let mut evm = self.evm_config().evm_with_env(db, evm_env);
|
||||||
|
apply_precompiles(&mut evm, &hl_extras);
|
||||||
|
let res = evm.transact(tx_env).map_err(Self::Error::from_evm_err)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transact_with_inspector<DB, I>(
|
||||||
|
&self,
|
||||||
|
db: DB,
|
||||||
|
evm_env: EvmEnvFor<Self::Evm>,
|
||||||
|
tx_env: TxEnvFor<Self::Evm>,
|
||||||
|
inspector: I,
|
||||||
|
) -> Result<ResultAndState<HaltReasonFor<Self::Evm>>, Self::Error>
|
||||||
|
where
|
||||||
|
DB: Database<Error = ProviderError> + fmt::Debug,
|
||||||
|
I: InspectorFor<Self::Evm, DB>,
|
||||||
|
{
|
||||||
|
let block_number = evm_env.block_env().number;
|
||||||
|
let hl_extras = self.get_hl_extras(block_number.try_into().unwrap())?;
|
||||||
|
|
||||||
|
let mut evm = self.evm_config().evm_with_env_and_inspector(db, evm_env, inspector);
|
||||||
|
apply_precompiles(&mut evm, &hl_extras);
|
||||||
|
let res = evm.transact(tx_env).map_err(Self::Error::from_evm_err)?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
fn replay_transactions_until<'a, DB, I>(
|
fn replay_transactions_until<'a, DB, I>(
|
||||||
&self,
|
&self,
|
||||||
db: &mut DB,
|
db: &mut DB,
|
||||||
|
|||||||
215
src/node/rpc/estimate.rs
Normal file
215
src/node/rpc/estimate.rs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
use super::{apply_precompiles, HlEthApi, HlRpcNodeCore};
|
||||||
|
use alloy_evm::overrides::{apply_state_overrides, StateOverrideError};
|
||||||
|
use alloy_network::TransactionBuilder;
|
||||||
|
use alloy_primitives::{TxKind, U256};
|
||||||
|
use alloy_rpc_types_eth::state::StateOverride;
|
||||||
|
use reth_chainspec::MIN_TRANSACTION_GAS;
|
||||||
|
use reth_errors::ProviderError;
|
||||||
|
use reth_evm::{ConfigureEvm, Evm, EvmEnvFor, SpecFor, TransactionEnv, TxEnvFor};
|
||||||
|
use reth_revm::{database::StateProviderDatabase, db::CacheDB};
|
||||||
|
use reth_rpc_convert::{RpcConvert, RpcTxReq};
|
||||||
|
use reth_rpc_eth_api::{
|
||||||
|
helpers::{
|
||||||
|
estimate::{update_estimated_gas_range, EstimateCall},
|
||||||
|
Call,
|
||||||
|
},
|
||||||
|
AsEthApiError, IntoEthApiError, RpcNodeCore,
|
||||||
|
};
|
||||||
|
use reth_rpc_eth_types::{
|
||||||
|
error::{api::FromEvmHalt, FromEvmError},
|
||||||
|
EthApiError, RevertError, RpcInvalidTransactionError,
|
||||||
|
};
|
||||||
|
use reth_rpc_server_types::constants::gas_oracle::{CALL_STIPEND_GAS, ESTIMATE_GAS_ERROR_RATIO};
|
||||||
|
use reth_storage_api::StateProvider;
|
||||||
|
use revm::context_interface::{result::ExecutionResult, Transaction};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
impl<N, Rpc> EstimateCall for HlEthApi<N, Rpc>
|
||||||
|
where
|
||||||
|
Self: Call,
|
||||||
|
N: HlRpcNodeCore,
|
||||||
|
EthApiError: FromEvmError<N::Evm> + From<StateOverrideError<ProviderError>>,
|
||||||
|
Rpc: RpcConvert<
|
||||||
|
Primitives = N::Primitives,
|
||||||
|
Error = EthApiError,
|
||||||
|
TxEnv = TxEnvFor<N::Evm>,
|
||||||
|
Spec = SpecFor<N::Evm>,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
// Modified version that adds `apply_precompiles`; comments are stripped out.
|
||||||
|
fn estimate_gas_with<S>(
|
||||||
|
&self,
|
||||||
|
mut evm_env: EvmEnvFor<Self::Evm>,
|
||||||
|
mut request: RpcTxReq<<Self::RpcConvert as RpcConvert>::Network>,
|
||||||
|
state: S,
|
||||||
|
state_override: Option<StateOverride>,
|
||||||
|
) -> Result<U256, Self::Error>
|
||||||
|
where
|
||||||
|
S: StateProvider,
|
||||||
|
{
|
||||||
|
evm_env.cfg_env.disable_eip3607 = true;
|
||||||
|
evm_env.cfg_env.disable_base_fee = true;
|
||||||
|
|
||||||
|
request.as_mut().take_nonce();
|
||||||
|
|
||||||
|
let tx_request_gas_limit = request.as_ref().gas_limit();
|
||||||
|
let tx_request_gas_price = request.as_ref().gas_price();
|
||||||
|
let max_gas_limit = evm_env
|
||||||
|
.cfg_env
|
||||||
|
.tx_gas_limit_cap
|
||||||
|
.map_or(evm_env.block_env.gas_limit, |cap| cap.min(evm_env.block_env.gas_limit));
|
||||||
|
|
||||||
|
let mut highest_gas_limit = tx_request_gas_limit
|
||||||
|
.map(|mut tx_gas_limit| {
|
||||||
|
if max_gas_limit < tx_gas_limit {
|
||||||
|
tx_gas_limit = max_gas_limit;
|
||||||
|
}
|
||||||
|
tx_gas_limit
|
||||||
|
})
|
||||||
|
.unwrap_or(max_gas_limit);
|
||||||
|
|
||||||
|
let mut db = CacheDB::new(StateProviderDatabase::new(state));
|
||||||
|
|
||||||
|
if let Some(state_override) = state_override {
|
||||||
|
apply_state_overrides(state_override, &mut db).map_err(
|
||||||
|
|err: StateOverrideError<ProviderError>| {
|
||||||
|
let eth_api_error: EthApiError = EthApiError::from(err);
|
||||||
|
Self::Error::from(eth_api_error)
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx_env = self.create_txn_env(&evm_env, request, &mut db)?;
|
||||||
|
|
||||||
|
let mut is_basic_transfer = false;
|
||||||
|
if tx_env.input().is_empty() {
|
||||||
|
if let TxKind::Call(to) = tx_env.kind() {
|
||||||
|
if let Ok(code) = db.db.account_code(&to) {
|
||||||
|
is_basic_transfer = code.map(|code| code.is_empty()).unwrap_or(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx_env.gas_price() > 0 {
|
||||||
|
highest_gas_limit =
|
||||||
|
highest_gas_limit.min(self.caller_gas_allowance(&mut db, &evm_env, &tx_env)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx_env.set_gas_limit(tx_env.gas_limit().min(highest_gas_limit));
|
||||||
|
|
||||||
|
let block_number = evm_env.block_env().number;
|
||||||
|
let hl_extras = self.get_hl_extras(block_number.try_into().unwrap())?;
|
||||||
|
|
||||||
|
let mut evm = self.evm_config().evm_with_env(&mut db, evm_env);
|
||||||
|
apply_precompiles(&mut evm, &hl_extras);
|
||||||
|
|
||||||
|
if is_basic_transfer {
|
||||||
|
let mut min_tx_env = tx_env.clone();
|
||||||
|
min_tx_env.set_gas_limit(MIN_TRANSACTION_GAS);
|
||||||
|
|
||||||
|
if let Ok(res) = evm.transact(min_tx_env).map_err(Self::Error::from_evm_err) {
|
||||||
|
if res.result.is_success() {
|
||||||
|
return Ok(U256::from(MIN_TRANSACTION_GAS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(target: "rpc::eth::estimate", ?tx_env, gas_limit = tx_env.gas_limit(), is_basic_transfer, "Starting gas estimation");
|
||||||
|
|
||||||
|
let mut res = match evm.transact(tx_env.clone()).map_err(Self::Error::from_evm_err) {
|
||||||
|
Err(err)
|
||||||
|
if err.is_gas_too_high() &&
|
||||||
|
(tx_request_gas_limit.is_some() || tx_request_gas_price.is_some()) =>
|
||||||
|
{
|
||||||
|
return Self::map_out_of_gas_err(&mut evm, tx_env, max_gas_limit);
|
||||||
|
}
|
||||||
|
Err(err) if err.is_gas_too_low() => {
|
||||||
|
return Err(RpcInvalidTransactionError::GasRequiredExceedsAllowance {
|
||||||
|
gas_limit: tx_env.gas_limit(),
|
||||||
|
}
|
||||||
|
.into_eth_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
ethres => ethres?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let gas_refund = match res.result {
|
||||||
|
ExecutionResult::Success { gas_refunded, .. } => gas_refunded,
|
||||||
|
ExecutionResult::Halt { reason, .. } => {
|
||||||
|
return Err(Self::Error::from_evm_halt(reason, tx_env.gas_limit()));
|
||||||
|
}
|
||||||
|
ExecutionResult::Revert { output, .. } => {
|
||||||
|
return if tx_request_gas_limit.is_some() || tx_request_gas_price.is_some() {
|
||||||
|
Self::map_out_of_gas_err(&mut evm, tx_env, max_gas_limit)
|
||||||
|
} else {
|
||||||
|
Err(RpcInvalidTransactionError::Revert(RevertError::new(output)).into_eth_err())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
highest_gas_limit = tx_env.gas_limit();
|
||||||
|
|
||||||
|
let mut gas_used = res.result.gas_used();
|
||||||
|
|
||||||
|
let mut lowest_gas_limit = gas_used.saturating_sub(1);
|
||||||
|
|
||||||
|
let optimistic_gas_limit = (gas_used + gas_refund + CALL_STIPEND_GAS) * 64 / 63;
|
||||||
|
if optimistic_gas_limit < highest_gas_limit {
|
||||||
|
let mut optimistic_tx_env = tx_env.clone();
|
||||||
|
optimistic_tx_env.set_gas_limit(optimistic_gas_limit);
|
||||||
|
|
||||||
|
res = evm.transact(optimistic_tx_env).map_err(Self::Error::from_evm_err)?;
|
||||||
|
|
||||||
|
gas_used = res.result.gas_used();
|
||||||
|
|
||||||
|
update_estimated_gas_range(
|
||||||
|
res.result,
|
||||||
|
optimistic_gas_limit,
|
||||||
|
&mut highest_gas_limit,
|
||||||
|
&mut lowest_gas_limit,
|
||||||
|
)?;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut mid_gas_limit = std::cmp::min(
|
||||||
|
gas_used * 3,
|
||||||
|
((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64,
|
||||||
|
);
|
||||||
|
|
||||||
|
trace!(target: "rpc::eth::estimate", ?highest_gas_limit, ?lowest_gas_limit, ?mid_gas_limit, "Starting binary search for gas");
|
||||||
|
|
||||||
|
while lowest_gas_limit + 1 < highest_gas_limit {
|
||||||
|
if (highest_gas_limit - lowest_gas_limit) as f64 / (highest_gas_limit as f64) <
|
||||||
|
ESTIMATE_GAS_ERROR_RATIO
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut mid_tx_env = tx_env.clone();
|
||||||
|
mid_tx_env.set_gas_limit(mid_gas_limit);
|
||||||
|
|
||||||
|
match evm.transact(mid_tx_env).map_err(Self::Error::from_evm_err) {
|
||||||
|
Err(err) if err.is_gas_too_high() => {
|
||||||
|
highest_gas_limit = mid_gas_limit;
|
||||||
|
}
|
||||||
|
Err(err) if err.is_gas_too_low() => {
|
||||||
|
lowest_gas_limit = mid_gas_limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ethres => {
|
||||||
|
res = ethres?;
|
||||||
|
|
||||||
|
update_estimated_gas_range(
|
||||||
|
res.result,
|
||||||
|
mid_gas_limit,
|
||||||
|
&mut highest_gas_limit,
|
||||||
|
&mut lowest_gas_limit,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mid_gas_limit = ((highest_gas_limit as u128 + lowest_gas_limit as u128) / 2) as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(U256::from(highest_gas_limit))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,6 +42,7 @@ use std::{fmt, marker::PhantomData, sync::Arc};
|
|||||||
mod block;
|
mod block;
|
||||||
mod call;
|
mod call;
|
||||||
pub mod engine_api;
|
pub mod engine_api;
|
||||||
|
mod estimate;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
|
||||||
pub trait HlRpcNodeCore: RpcNodeCore<Primitives: NodePrimitives<Block = HlBlock>> {}
|
pub trait HlRpcNodeCore: RpcNodeCore<Primitives: NodePrimitives<Block = HlBlock>> {}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ use std::collections::BTreeMap;
|
|||||||
|
|
||||||
use crate::chainspec::{MAINNET_CHAIN_ID, TESTNET_CHAIN_ID};
|
use crate::chainspec::{MAINNET_CHAIN_ID, TESTNET_CHAIN_ID};
|
||||||
|
|
||||||
|
mod patch;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
struct EvmContract {
|
struct EvmContract {
|
||||||
address: Address,
|
address: Address,
|
||||||
@ -58,5 +60,10 @@ pub(crate) fn erc20_contract_to_spot_token(chain_id: u64) -> Result<BTreeMap<Add
|
|||||||
map.insert(evm_contract.address, SpotId { index: token.index });
|
map.insert(evm_contract.address, SpotId { index: token.index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if chain_id == TESTNET_CHAIN_ID {
|
||||||
|
patch::patch_testnet_spot_meta(&mut map);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
8
src/node/spot_meta/patch.rs
Normal file
8
src/node/spot_meta/patch.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use crate::node::spot_meta::SpotId;
|
||||||
|
use alloy_primitives::{address, Address};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
/// Testnet-specific fix for #67
|
||||||
|
pub(super) fn patch_testnet_spot_meta(map: &mut BTreeMap<Address, SpotId>) {
|
||||||
|
map.insert(address!("0xd9cbec81df392a88aeff575e962d149d57f4d6bc"), SpotId { index: 0 });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user