feat(ABCI): New Proposal Struct with Associated Metadata (#126)
* new proto types for proposal info * new proposal type * nits * lane input * lint * feat(ABCI): Deprecating `CheckOrderHandler` with new Proposal MetaData (#127) * refactor without checkorder * nits * more nits * lint * nits * feat(ABCI): Updating MEV lane to have no `CheckOrder` handler + testing (#128) * updating mev lane * nits * preventing adding multiple bid txs in prepare * update
This commit is contained in:
+68
-20
@@ -4,20 +4,24 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/skip-mev/block-sdk/block"
|
||||
"github.com/skip-mev/block-sdk/block/proposals"
|
||||
"github.com/skip-mev/block-sdk/block/utils"
|
||||
)
|
||||
|
||||
// PrepareLane will prepare a partial proposal for the lane. It will select transactions from the
|
||||
// lane respecting the selection logic of the prepareLaneHandler. It will then update the partial
|
||||
// proposal with the selected transactions. If the proposal is unable to be updated, we return an
|
||||
// error. The proposal will only be modified if it passes all of the invarient checks.
|
||||
// error. The proposal will only be modified if it passes all of the invariant checks.
|
||||
func (l *BaseLane) PrepareLane(
|
||||
ctx sdk.Context,
|
||||
proposal block.BlockProposal,
|
||||
maxTxBytes int64,
|
||||
proposal proposals.Proposal,
|
||||
next block.PrepareLanesHandler,
|
||||
) (block.BlockProposal, error) {
|
||||
txs, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, maxTxBytes)
|
||||
) (proposals.Proposal, error) {
|
||||
limit := proposal.GetLaneLimits(l.cfg.MaxBlockSpace)
|
||||
|
||||
// Select transactions from the lane respecting the selection logic of the lane and the
|
||||
// max block space for the lane.
|
||||
txsToInclude, txsToRemove, err := l.prepareLaneHandler(ctx, proposal, limit)
|
||||
if err != nil {
|
||||
return proposal, err
|
||||
}
|
||||
@@ -31,31 +35,75 @@ func (l *BaseLane) PrepareLane(
|
||||
)
|
||||
}
|
||||
|
||||
// Update the proposal with the selected transactions.
|
||||
if err := proposal.UpdateProposal(l, txs); err != nil {
|
||||
// Update the proposal with the selected transactions. This fails if the lane attempted to add
|
||||
// more transactions than the allocated max block space for the lane.
|
||||
if err := proposal.UpdateProposal(l, txsToInclude); err != nil {
|
||||
l.Logger().Error(
|
||||
"failed to update proposal",
|
||||
"lane", l.Name(),
|
||||
"err", err,
|
||||
"num_txs_to_add", len(txsToInclude),
|
||||
"num_txs_to_remove", len(txsToRemove),
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
l.Logger().Info(
|
||||
"lane prepared",
|
||||
"lane", l.Name(),
|
||||
"num_txs_added", len(txsToInclude),
|
||||
"num_txs_removed", len(txsToRemove),
|
||||
)
|
||||
|
||||
return next(ctx, proposal)
|
||||
}
|
||||
|
||||
// CheckOrder checks that the ordering logic of the lane is respected given the set of transactions
|
||||
// in the block proposal. If the ordering logic is not respected, we return an error.
|
||||
func (l *BaseLane) CheckOrder(ctx sdk.Context, txs []sdk.Tx) error {
|
||||
return l.checkOrderHandler(ctx, txs)
|
||||
}
|
||||
|
||||
// ProcessLane verifies that the transactions included in the block proposal are valid respecting
|
||||
// the verification logic of the lane (processLaneHandler). If the transactions are valid, we
|
||||
// return the transactions that do not belong to this lane to the next lane. If the transactions
|
||||
// are invalid, we return an error.
|
||||
func (l *BaseLane) ProcessLane(ctx sdk.Context, txs []sdk.Tx, next block.ProcessLanesHandler) (sdk.Context, error) {
|
||||
remainingTxs, err := l.processLaneHandler(ctx, txs)
|
||||
// the verification logic of the lane (processLaneHandler). If any of the transactions are invalid,
|
||||
// we return an error. If all of the transactions are valid, we return the updated proposal.
|
||||
func (l *BaseLane) ProcessLane(
|
||||
ctx sdk.Context,
|
||||
proposal proposals.Proposal,
|
||||
txs [][]byte,
|
||||
next block.ProcessLanesHandler,
|
||||
) (proposals.Proposal, error) {
|
||||
// Assume that this lane is processing sdk.Tx's and decode the transactions.
|
||||
decodedTxs, err := utils.GetDecodedTxs(l.TxDecoder(), txs)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
l.Logger().Error(
|
||||
"failed to decode transactions",
|
||||
"lane", l.Name(),
|
||||
"err", err,
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
return next(ctx, remainingTxs)
|
||||
// Verify the transactions that belong to this lane according to the verification logic of the lane.
|
||||
if err := l.processLaneHandler(ctx, decodedTxs); err != nil {
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
// Optimistically update the proposal with the partial proposal.
|
||||
if err := proposal.UpdateProposal(l, decodedTxs); err != nil {
|
||||
l.Logger().Error(
|
||||
"failed to update proposal",
|
||||
"lane", l.Name(),
|
||||
"err", err,
|
||||
"num_txs_to_verify", len(decodedTxs),
|
||||
)
|
||||
|
||||
return proposal, err
|
||||
}
|
||||
|
||||
l.Logger().Info(
|
||||
"lane processed",
|
||||
"lane", l.Name(),
|
||||
"num_txs_verified", len(decodedTxs),
|
||||
)
|
||||
|
||||
return next(ctx, proposal)
|
||||
}
|
||||
|
||||
// AnteVerifyTx verifies that the transaction is valid respecting the ante verification logic of
|
||||
|
||||
+55
-65
@@ -5,20 +5,21 @@ import (
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/skip-mev/block-sdk/block"
|
||||
"github.com/skip-mev/block-sdk/block/proposals"
|
||||
"github.com/skip-mev/block-sdk/block/utils"
|
||||
)
|
||||
|
||||
// DefaultPrepareLaneHandler returns a default implementation of the PrepareLaneHandler. It
|
||||
// selects all transactions in the mempool that are valid and not already in the partial
|
||||
// proposal. It will continue to reap transactions until the maximum block space for this
|
||||
// proposal. It will continue to reap transactions until the maximum blockspace/gas for this
|
||||
// lane has been reached. Additionally, any transactions that are invalid will be returned.
|
||||
func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) ([][]byte, []sdk.Tx, error) {
|
||||
return func(ctx sdk.Context, proposal proposals.Proposal, limit proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
|
||||
var (
|
||||
totalSize int64
|
||||
txs [][]byte
|
||||
txsToRemove []sdk.Tx
|
||||
totalSize int64
|
||||
totalGas uint64
|
||||
txsToInclude []sdk.Tx
|
||||
txsToRemove []sdk.Tx
|
||||
)
|
||||
|
||||
// Select all transactions in the mempool that are valid and not already in the
|
||||
@@ -26,7 +27,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
for iterator := l.Select(ctx, nil); iterator != nil; iterator = iterator.Next() {
|
||||
tx := iterator.Tx()
|
||||
|
||||
txBytes, hash, err := utils.GetTxHashStr(l.TxEncoder(), tx)
|
||||
txInfo, err := utils.GetTxInfo(l.TxEncoder(), tx)
|
||||
if err != nil {
|
||||
l.Logger().Info("failed to get hash of tx", "err", err)
|
||||
|
||||
@@ -38,7 +39,7 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
if !l.Match(ctx, tx) {
|
||||
l.Logger().Info(
|
||||
"failed to select tx for lane; tx does not belong to lane",
|
||||
"tx_hash", hash,
|
||||
"tx_hash", txInfo.Hash,
|
||||
"lane", l.Name(),
|
||||
)
|
||||
|
||||
@@ -47,10 +48,10 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
}
|
||||
|
||||
// if the transaction is already in the (partial) block proposal, we skip it.
|
||||
if proposal.Contains(txBytes) {
|
||||
if proposal.Contains(txInfo.Hash) {
|
||||
l.Logger().Info(
|
||||
"failed to select tx for lane; tx is already in proposal",
|
||||
"tx_hash", hash,
|
||||
"tx_hash", txInfo.Hash,
|
||||
"lane", l.Name(),
|
||||
)
|
||||
|
||||
@@ -58,25 +59,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
}
|
||||
|
||||
// If the transaction is too large, we break and do not attempt to include more txs.
|
||||
txSize := int64(len(txBytes))
|
||||
if updatedSize := totalSize + txSize; updatedSize > maxTxBytes {
|
||||
if updatedSize := totalSize + txInfo.Size; updatedSize > limit.MaxTxBytes {
|
||||
l.Logger().Info(
|
||||
"tx bytes above the maximum allowed",
|
||||
"failed to select tx for lane; tx bytes above the maximum allowed",
|
||||
"lane", l.Name(),
|
||||
"tx_size", txSize,
|
||||
"tx_size", txInfo.Size,
|
||||
"total_size", totalSize,
|
||||
"max_tx_bytes", maxTxBytes,
|
||||
"tx_hash", hash,
|
||||
"max_tx_bytes", limit.MaxTxBytes,
|
||||
"tx_hash", txInfo.Hash,
|
||||
)
|
||||
|
||||
break
|
||||
// TODO: Determine if there is any trade off with breaking or continuing here.
|
||||
continue
|
||||
}
|
||||
|
||||
// If the gas limit of the transaction is too large, we break and do not attempt to include more txs.
|
||||
if updatedGas := totalGas + txInfo.GasLimit; updatedGas > limit.MaxGasLimit {
|
||||
l.Logger().Info(
|
||||
"failed to select tx for lane; gas limit above the maximum allowed",
|
||||
"lane", l.Name(),
|
||||
"tx_gas", txInfo.GasLimit,
|
||||
"total_gas", totalGas,
|
||||
"max_gas", limit.MaxGasLimit,
|
||||
"tx_hash", txInfo.Hash,
|
||||
)
|
||||
|
||||
// TODO: Determine if there is any trade off with breaking or continuing here.
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify the transaction.
|
||||
if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil {
|
||||
l.Logger().Info(
|
||||
"failed to verify tx",
|
||||
"tx_hash", hash,
|
||||
"tx_hash", txInfo.Hash,
|
||||
"err", err,
|
||||
)
|
||||
|
||||
@@ -84,66 +100,40 @@ func (l *BaseLane) DefaultPrepareLaneHandler() PrepareLaneHandler {
|
||||
continue
|
||||
}
|
||||
|
||||
totalSize += txSize
|
||||
txs = append(txs, txBytes)
|
||||
totalSize += txInfo.Size
|
||||
totalGas += txInfo.GasLimit
|
||||
txsToInclude = append(txsToInclude, tx)
|
||||
}
|
||||
|
||||
return txs, txsToRemove, nil
|
||||
return txsToInclude, txsToRemove, nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It
|
||||
// verifies all transactions in the lane that matches to the lane. If any transaction
|
||||
// fails to verify, the entire proposal is rejected. If the handler comes across a transaction
|
||||
// that does not match the lane's matcher, it will return the remaining transactions in the
|
||||
// proposal.
|
||||
// DefaultProcessLaneHandler returns a default implementation of the ProcessLaneHandler. It verifies
|
||||
// the following invariants:
|
||||
// 1. All transactions belong to this lane.
|
||||
// 2. All transactions respect the priority defined by the mempool.
|
||||
// 3. All transactions are valid respecting the verification logic of the lane.
|
||||
func (l *BaseLane) DefaultProcessLaneHandler() ProcessLaneHandler {
|
||||
return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) {
|
||||
var err error
|
||||
|
||||
return func(ctx sdk.Context, partialProposal []sdk.Tx) error {
|
||||
// Process all transactions that match the lane's matcher.
|
||||
for index, tx := range txs {
|
||||
if l.Match(ctx, tx) {
|
||||
if ctx, err = l.AnteVerifyTx(ctx, tx, false); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify tx: %w", err)
|
||||
}
|
||||
} else {
|
||||
return txs[index:], nil
|
||||
for index, tx := range partialProposal {
|
||||
if !l.Match(ctx, tx) {
|
||||
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// This means we have processed all transactions in the proposal.
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultCheckOrderHandler returns a default implementation of the CheckOrderHandler. It
|
||||
// ensures the following invariants:
|
||||
//
|
||||
// 1. All transactions that belong to this lane respect the ordering logic defined by the
|
||||
// lane.
|
||||
// 2. Transactions that belong to other lanes cannot be interleaved with transactions that
|
||||
// belong to this lane.
|
||||
func (l *BaseLane) DefaultCheckOrderHandler() CheckOrderHandler {
|
||||
return func(ctx sdk.Context, txs []sdk.Tx) error {
|
||||
seenOtherLaneTx := false
|
||||
|
||||
for index, tx := range txs {
|
||||
if l.Match(ctx, tx) {
|
||||
if seenOtherLaneTx {
|
||||
return fmt.Errorf("the %s lane contains a transaction that belongs to another lane", l.Name())
|
||||
}
|
||||
// If the transactions do not respect the priority defined by the mempool, we consider the proposal
|
||||
// to be invalid
|
||||
if index > 0 && l.Compare(ctx, partialProposal[index-1], tx) == -1 {
|
||||
return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
|
||||
}
|
||||
|
||||
// If the transactions do not respect the priority defined by the mempool, we consider the proposal
|
||||
// to be invalid
|
||||
if index > 0 && l.Compare(ctx, txs[index-1], tx) == -1 {
|
||||
return fmt.Errorf("transaction at index %d has a higher priority than %d", index, index-1)
|
||||
}
|
||||
} else {
|
||||
seenOtherLaneTx = true
|
||||
if _, err := l.AnteVerifyTx(ctx, tx, false); err != nil {
|
||||
return fmt.Errorf("failed to verify tx: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// This means we have processed all transactions in the partial proposal.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,6 @@ type BaseLane struct { //nolint
|
||||
// requested and the lane needs to submit transactions it wants included in the block.
|
||||
prepareLaneHandler PrepareLaneHandler
|
||||
|
||||
// checkOrderHandler is the function that is called when a new proposal is being
|
||||
// verified and the lane needs to verify that the transactions included in the proposal
|
||||
// respect the ordering rules of the lane and does not interleave transactions from other lanes.
|
||||
checkOrderHandler CheckOrderHandler
|
||||
|
||||
// processLaneHandler is the function that is called when a new proposal is being
|
||||
// verified and the lane needs to verify that the transactions included in the proposal
|
||||
// are valid respecting the verification logic of the lane.
|
||||
@@ -95,10 +90,6 @@ func (l *BaseLane) ValidateBasic() error {
|
||||
l.processLaneHandler = l.DefaultProcessLaneHandler()
|
||||
}
|
||||
|
||||
if l.checkOrderHandler == nil {
|
||||
l.checkOrderHandler = l.DefaultCheckOrderHandler()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,18 +116,6 @@ func (l *BaseLane) SetProcessLaneHandler(processLaneHandler ProcessLaneHandler)
|
||||
l.processLaneHandler = processLaneHandler
|
||||
}
|
||||
|
||||
// SetCheckOrderHandler sets the check order handler for the lane. This handler
|
||||
// is called when a new proposal is being verified and the lane needs to verify
|
||||
// that the transactions included in the proposal respect the ordering rules of
|
||||
// the lane and does not include transactions from other lanes.
|
||||
func (l *BaseLane) SetCheckOrderHandler(checkOrderHandler CheckOrderHandler) {
|
||||
if checkOrderHandler == nil {
|
||||
panic("check order handler cannot be nil")
|
||||
}
|
||||
|
||||
l.checkOrderHandler = checkOrderHandler
|
||||
}
|
||||
|
||||
// Match returns true if the transaction should be processed by this lane. This
|
||||
// function first determines if the transaction matches the lane and then checks
|
||||
// if the transaction is on the ignore list. If the transaction is on the ignore
|
||||
|
||||
@@ -103,13 +103,13 @@ func (cm *Mempool[C]) Insert(ctx context.Context, tx sdk.Tx) error {
|
||||
return fmt.Errorf("failed to insert tx into auction index: %w", err)
|
||||
}
|
||||
|
||||
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
|
||||
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
|
||||
if err != nil {
|
||||
cm.Remove(tx)
|
||||
return err
|
||||
}
|
||||
|
||||
cm.txCache[txHashStr] = struct{}{}
|
||||
cm.txCache[txInfo.Hash] = struct{}{}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -120,12 +120,12 @@ func (cm *Mempool[C]) Remove(tx sdk.Tx) error {
|
||||
return fmt.Errorf("failed to remove transaction from the mempool: %w", err)
|
||||
}
|
||||
|
||||
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
|
||||
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tx hash string: %w", err)
|
||||
}
|
||||
|
||||
delete(cm.txCache, txHashStr)
|
||||
delete(cm.txCache, txInfo.Hash)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -145,12 +145,12 @@ func (cm *Mempool[C]) CountTx() int {
|
||||
|
||||
// Contains returns true if the transaction is contained in the mempool.
|
||||
func (cm *Mempool[C]) Contains(tx sdk.Tx) bool {
|
||||
_, txHashStr, err := utils.GetTxHashStr(cm.txEncoder, tx)
|
||||
txInfo, err := utils.GetTxInfo(cm.txEncoder, tx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok := cm.txCache[txHashStr]
|
||||
_, ok := cm.txCache[txInfo.Hash]
|
||||
return ok
|
||||
}
|
||||
|
||||
|
||||
+12
-20
@@ -3,7 +3,7 @@ package base
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/skip-mev/block-sdk/block"
|
||||
"github.com/skip-mev/block-sdk/block/proposals"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -16,28 +16,20 @@ type (
|
||||
// the transactions that must be removed from the lane, and an error if one occurred.
|
||||
PrepareLaneHandler func(
|
||||
ctx sdk.Context,
|
||||
proposal block.BlockProposal,
|
||||
maxTxBytes int64,
|
||||
) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error)
|
||||
proposal proposals.Proposal,
|
||||
limit proposals.LaneLimits,
|
||||
) (txsToInclude []sdk.Tx, txsToRemove []sdk.Tx, err error)
|
||||
|
||||
// ProcessLaneHandler is responsible for processing transactions that are included in a block and
|
||||
// belong to a given lane. ProcessLaneHandler is executed after CheckOrderHandler so the transactions
|
||||
// passed into this function SHOULD already be in order respecting the ordering rules of the lane and
|
||||
// respecting the ordering rules of mempool relative to the lanes it has.
|
||||
ProcessLaneHandler func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error)
|
||||
|
||||
// CheckOrderHandler is responsible for checking the order of transactions that belong to a given
|
||||
// lane. This handler should be used to verify that the ordering of transactions passed into the
|
||||
// function respect the ordering logic of the lane (if any transactions from the lane are included).
|
||||
// This function should also ensure that transactions that belong to this lane are contiguous and do
|
||||
// not have any transactions from other lanes in between them.
|
||||
CheckOrderHandler func(ctx sdk.Context, txs []sdk.Tx) error
|
||||
// belong to a given lane. This handler must return an error if the transactions are not correctly
|
||||
// ordered, do not belong to this lane, or any other relevant error.
|
||||
ProcessLaneHandler func(ctx sdk.Context, partialProposal []sdk.Tx) error
|
||||
)
|
||||
|
||||
// NoOpPrepareLaneHandler returns a no-op prepare lane handler.
|
||||
// This should only be used for testing.
|
||||
func NoOpPrepareLaneHandler() PrepareLaneHandler {
|
||||
return func(ctx sdk.Context, proposal block.BlockProposal, maxTxBytes int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) {
|
||||
return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
}
|
||||
@@ -45,7 +37,7 @@ func NoOpPrepareLaneHandler() PrepareLaneHandler {
|
||||
// PanicPrepareLaneHandler returns a prepare lane handler that panics.
|
||||
// This should only be used for testing.
|
||||
func PanicPrepareLaneHandler() PrepareLaneHandler {
|
||||
return func(sdk.Context, block.BlockProposal, int64) (txsToInclude [][]byte, txsToRemove []sdk.Tx, err error) {
|
||||
return func(sdk.Context, proposals.Proposal, proposals.LaneLimits) ([]sdk.Tx, []sdk.Tx, error) {
|
||||
panic("panic prepare lanes handler")
|
||||
}
|
||||
}
|
||||
@@ -53,15 +45,15 @@ func PanicPrepareLaneHandler() PrepareLaneHandler {
|
||||
// NoOpProcessLaneHandler returns a no-op process lane handler.
|
||||
// This should only be used for testing.
|
||||
func NoOpProcessLaneHandler() ProcessLaneHandler {
|
||||
return func(ctx sdk.Context, txs []sdk.Tx) ([]sdk.Tx, error) {
|
||||
return txs, nil
|
||||
return func(sdk.Context, []sdk.Tx) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// PanicProcessLanesHandler returns a process lanes handler that panics.
|
||||
// This should only be used for testing.
|
||||
func PanicProcessLaneHandler() ProcessLaneHandler {
|
||||
return func(sdk.Context, []sdk.Tx) ([]sdk.Tx, error) {
|
||||
return func(sdk.Context, []sdk.Tx) error {
|
||||
panic("panic process lanes handler")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user