mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-03-03 08:46:05 +00:00
legacy: Stream prev_tx after confirmation.
This commit is contained in:
parent
4fc4152741
commit
e9ed0851b3
@ -39,13 +39,14 @@ static CONFIDENTIAL HDNode node;
|
||||
static bool signing = false;
|
||||
enum {
|
||||
STAGE_REQUEST_1_INPUT,
|
||||
STAGE_REQUEST_2_PREV_META,
|
||||
STAGE_REQUEST_2_PREV_INPUT,
|
||||
STAGE_REQUEST_2_PREV_OUTPUT,
|
||||
STAGE_REQUEST_2_OUTPUT,
|
||||
STAGE_REQUEST_3_INPUT,
|
||||
STAGE_REQUEST_3_PREV_META,
|
||||
STAGE_REQUEST_3_PREV_INPUT,
|
||||
STAGE_REQUEST_3_PREV_OUTPUT,
|
||||
#if !BITCOIN_ONLY
|
||||
STAGE_REQUEST_2_PREV_EXTRADATA,
|
||||
STAGE_REQUEST_3_PREV_EXTRADATA,
|
||||
#endif
|
||||
STAGE_REQUEST_3_OUTPUT,
|
||||
STAGE_REQUEST_4_INPUT,
|
||||
STAGE_REQUEST_4_OUTPUT,
|
||||
STAGE_REQUEST_SEGWIT_INPUT,
|
||||
@ -68,7 +69,7 @@ static uint8_t hash_prevouts[32], hash_sequence[32], hash_outputs[32];
|
||||
#if !BITCOIN_ONLY
|
||||
static uint8_t decred_hash_prefix[32];
|
||||
#endif
|
||||
static uint8_t hash_check[32];
|
||||
static uint8_t hash_inputs_check[32];
|
||||
static uint64_t to_spend, spending, change_spend;
|
||||
static uint32_t version = 1;
|
||||
static uint32_t lock_time = 0;
|
||||
@ -137,9 +138,10 @@ The STAGE_ constants describe the signing_stage when request is sent.
|
||||
I - input
|
||||
O - output
|
||||
|
||||
Phase1 - check inputs, previous transactions, and outputs
|
||||
- ask for confirmations
|
||||
- check fee
|
||||
Phase1 - process inputs
|
||||
- confirm outputs
|
||||
- check fee and confirm totals
|
||||
- check previous transactions
|
||||
=========================================================
|
||||
|
||||
foreach I (idx1):
|
||||
@ -149,17 +151,9 @@ foreach I (idx1):
|
||||
Add I to TransactionChecksum (prevout and type)
|
||||
if (Decred)
|
||||
Return I
|
||||
If not segwit, Calculate amount of I:
|
||||
Request prevhash I, META STAGE_REQUEST_2_PREV_META
|
||||
foreach prevhash I (idx2):
|
||||
Request prevhash I STAGE_REQUEST_2_PREV_INPUT
|
||||
foreach prevhash O (idx2):
|
||||
Request prevhash O STAGE_REQUEST_2_PREV_OUTPUT
|
||||
Add amount of prevhash O (which is amount of I)
|
||||
Request prevhash extra data (if applicable) STAGE_REQUEST_2_PREV_EXTRADATA
|
||||
Calculate hash of streamed tx, compare to prevhash I
|
||||
|
||||
foreach O (idx1):
|
||||
Request O STAGE_REQUEST_3_OUTPUT
|
||||
Request O STAGE_REQUEST_2_OUTPUT
|
||||
Add O to Decred decred_hash_prefix
|
||||
Add O to TransactionChecksum
|
||||
if (Decred)
|
||||
@ -170,6 +164,17 @@ foreach O (idx1):
|
||||
Check tx fee
|
||||
Ask for confirmation
|
||||
|
||||
foreach I (idx1):
|
||||
Request I STAGE_REQUEST_3_INPUT
|
||||
Request prevhash I, META STAGE_REQUEST_3_PREV_META
|
||||
foreach prevhash I (idx2):
|
||||
Request prevhash I STAGE_REQUEST_3_PREV_INPUT
|
||||
foreach prevhash O (idx2):
|
||||
Request prevhash O STAGE_REQUEST_3_PREV_OUTPUT
|
||||
Add amount of prevhash O (which is amount of I)
|
||||
Request prevhash extra data (if applicable) STAGE_REQUEST_3_PREV_EXTRADATA
|
||||
Calculate hash of streamed tx, compare to prevhash I
|
||||
|
||||
Phase2: sign inputs, check that nothing changed
|
||||
===============================================
|
||||
|
||||
@ -239,8 +244,28 @@ void send_req_1_input(void) {
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_2_prev_meta(void) {
|
||||
signing_stage = STAGE_REQUEST_2_PREV_META;
|
||||
void send_req_2_output(void) {
|
||||
signing_stage = STAGE_REQUEST_2_OUTPUT;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXOUTPUT;
|
||||
resp.has_details = true;
|
||||
resp.details.has_request_index = true;
|
||||
resp.details.request_index = idx1;
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_3_input(void) {
|
||||
signing_stage = STAGE_REQUEST_3_INPUT;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXINPUT;
|
||||
resp.has_details = true;
|
||||
resp.details.has_request_index = true;
|
||||
resp.details.request_index = idx1;
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_3_prev_meta(void) {
|
||||
signing_stage = STAGE_REQUEST_3_PREV_META;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXMETA;
|
||||
resp.has_details = true;
|
||||
@ -251,8 +276,8 @@ void send_req_2_prev_meta(void) {
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_2_prev_input(void) {
|
||||
signing_stage = STAGE_REQUEST_2_PREV_INPUT;
|
||||
void send_req_3_prev_input(void) {
|
||||
signing_stage = STAGE_REQUEST_3_PREV_INPUT;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXINPUT;
|
||||
resp.has_details = true;
|
||||
@ -265,8 +290,8 @@ void send_req_2_prev_input(void) {
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_2_prev_output(void) {
|
||||
signing_stage = STAGE_REQUEST_2_PREV_OUTPUT;
|
||||
void send_req_3_prev_output(void) {
|
||||
signing_stage = STAGE_REQUEST_3_PREV_OUTPUT;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXOUTPUT;
|
||||
resp.has_details = true;
|
||||
@ -281,8 +306,8 @@ void send_req_2_prev_output(void) {
|
||||
|
||||
#if !BITCOIN_ONLY
|
||||
|
||||
void send_req_2_prev_extradata(uint32_t chunk_offset, uint32_t chunk_len) {
|
||||
signing_stage = STAGE_REQUEST_2_PREV_EXTRADATA;
|
||||
void send_req_3_prev_extradata(uint32_t chunk_offset, uint32_t chunk_len) {
|
||||
signing_stage = STAGE_REQUEST_3_PREV_EXTRADATA;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXEXTRADATA;
|
||||
resp.has_details = true;
|
||||
@ -299,16 +324,6 @@ void send_req_2_prev_extradata(uint32_t chunk_offset, uint32_t chunk_len) {
|
||||
|
||||
#endif
|
||||
|
||||
void send_req_3_output(void) {
|
||||
signing_stage = STAGE_REQUEST_3_OUTPUT;
|
||||
resp.has_request_type = true;
|
||||
resp.request_type = RequestType_TXOUTPUT;
|
||||
resp.has_details = true;
|
||||
resp.details.has_request_index = true;
|
||||
resp.details.request_index = idx1;
|
||||
msg_write(MessageType_MessageType_TxRequest, &resp);
|
||||
}
|
||||
|
||||
void send_req_4_input(void) {
|
||||
signing_stage = STAGE_REQUEST_4_INPUT;
|
||||
resp.has_request_type = true;
|
||||
@ -387,11 +402,11 @@ void phase1_request_next_input(void) {
|
||||
// compute segwit hashPrevouts & hashSequence
|
||||
hasher_Final(&hasher_prevouts, hash_prevouts);
|
||||
hasher_Final(&hasher_sequence, hash_sequence);
|
||||
hasher_Final(&hasher_check, hash_check);
|
||||
hasher_Final(&hasher_check, hash_inputs_check);
|
||||
// init hashOutputs
|
||||
hasher_Reset(&hasher_outputs);
|
||||
idx1 = 0;
|
||||
send_req_3_output();
|
||||
send_req_2_output();
|
||||
}
|
||||
}
|
||||
|
||||
@ -812,12 +827,8 @@ static bool signing_check_input(const TxInputType *txinput) {
|
||||
tx_serialize_input_hash(&ti, txinput);
|
||||
}
|
||||
#endif
|
||||
|
||||
// hash prevout and script type to check it later (relevant for fee
|
||||
// computation)
|
||||
tx_prevout_hash(&hasher_check, txinput);
|
||||
hasher_Update(&hasher_check, (const uint8_t *)&txinput->script_type,
|
||||
sizeof(&txinput->script_type));
|
||||
// hash all input data to check it later (relevant for fee computation)
|
||||
tx_input_check_hash(&hasher_check, txinput);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -831,7 +842,34 @@ static bool signing_check_prevtx_hash(void) {
|
||||
signing_abort();
|
||||
return false;
|
||||
}
|
||||
phase1_request_next_input();
|
||||
|
||||
if (idx1 < inputs_count - 1) {
|
||||
idx1++;
|
||||
send_req_3_input();
|
||||
} else {
|
||||
hasher_Final(&hasher_check, hash);
|
||||
if (memcmp(hash, hash_inputs_check, 32) != 0) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Transaction has changed during signing"));
|
||||
signing_abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Everything was checked, now phase 2 begins and the transaction is signed.
|
||||
progress_meta_step = progress_step / (inputs_count + outputs_count);
|
||||
layoutProgress(_("Signing transaction"), progress);
|
||||
idx1 = 0;
|
||||
#if !BITCOIN_ONLY
|
||||
if (coin->decred) {
|
||||
// Decred prefix serialized in Phase 1, skip Phase 2
|
||||
send_req_decred_witness();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
phase2_request_next_input();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -986,7 +1024,7 @@ static uint32_t signing_hash_type(void) {
|
||||
static void phase1_request_next_output(void) {
|
||||
if (idx1 < outputs_count - 1) {
|
||||
idx1++;
|
||||
send_req_3_output();
|
||||
send_req_2_output();
|
||||
} else {
|
||||
#if !BITCOIN_ONLY
|
||||
if (coin->decred) {
|
||||
@ -998,19 +1036,8 @@ static void phase1_request_next_output(void) {
|
||||
if (!signing_confirm_tx()) {
|
||||
return;
|
||||
}
|
||||
// Everything was checked, now phase 2 begins and the transaction is signed.
|
||||
progress_meta_step = progress_step / (inputs_count + outputs_count);
|
||||
layoutProgress(_("Signing transaction"), progress);
|
||||
idx1 = 0;
|
||||
#if !BITCOIN_ONLY
|
||||
if (coin->decred) {
|
||||
// Decred prefix serialized in Phase 1, skip Phase 2
|
||||
send_req_decred_witness();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
phase2_request_next_input();
|
||||
}
|
||||
send_req_3_input();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1272,6 +1299,20 @@ void signing_txack(TransactionType *tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tx->inputs[0].has_amount) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Expected input with amount"));
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (to_spend + tx->inputs[0].amount < to_spend) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError, _("Value overflow"));
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
to_spend += tx->inputs[0].amount;
|
||||
|
||||
tx_weight += tx_input_weight(coin, &tx->inputs[0]);
|
||||
#if !BITCOIN_ONLY
|
||||
if (coin->decred) {
|
||||
@ -1279,8 +1320,6 @@ void signing_txack(TransactionType *tx) {
|
||||
}
|
||||
#endif
|
||||
|
||||
memcpy(&input, tx->inputs, sizeof(TxInputType));
|
||||
|
||||
if (tx->inputs[0].script_type == InputScriptType_SPENDMULTISIG ||
|
||||
tx->inputs[0].script_type == InputScriptType_SPENDADDRESS) {
|
||||
#if !ENABLE_SEGWIT_NONSEGWIT_MIXING
|
||||
@ -1323,9 +1362,38 @@ void signing_txack(TransactionType *tx) {
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
send_req_2_prev_meta();
|
||||
phase1_request_next_input();
|
||||
return;
|
||||
case STAGE_REQUEST_2_PREV_META:
|
||||
case STAGE_REQUEST_2_OUTPUT:
|
||||
if (!signing_validate_output(&tx->outputs[0]) ||
|
||||
!signing_check_output(&tx->outputs[0])) {
|
||||
return;
|
||||
}
|
||||
tx_weight += tx_output_weight(coin, &tx->outputs[0]);
|
||||
phase1_request_next_output();
|
||||
return;
|
||||
case STAGE_REQUEST_3_INPUT:
|
||||
if (!signing_validate_input(&tx->inputs[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tx->inputs[0].has_amount) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Expected input with amount"));
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx1 == 0) {
|
||||
hasher_Reset(&hasher_check);
|
||||
}
|
||||
tx_input_check_hash(&hasher_check, tx->inputs);
|
||||
|
||||
memcpy(&input, tx->inputs, sizeof(TxInputType));
|
||||
|
||||
send_req_3_prev_meta();
|
||||
return;
|
||||
case STAGE_REQUEST_3_PREV_META:
|
||||
if (tx->outputs_cnt <= input.prev_index) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Not enough outputs in previous transaction."));
|
||||
@ -1394,13 +1462,13 @@ void signing_txack(TransactionType *tx) {
|
||||
progress_meta_step = progress_step / (tp.inputs_len + tp.outputs_len);
|
||||
idx2 = 0;
|
||||
if (tp.inputs_len > 0) {
|
||||
send_req_2_prev_input();
|
||||
send_req_3_prev_input();
|
||||
} else {
|
||||
tx_serialize_header_hash(&tp);
|
||||
send_req_2_prev_output();
|
||||
send_req_3_prev_output();
|
||||
}
|
||||
return;
|
||||
case STAGE_REQUEST_2_PREV_INPUT:
|
||||
case STAGE_REQUEST_3_PREV_INPUT:
|
||||
if (!signing_validate_input(&tx->inputs[0])) {
|
||||
return;
|
||||
}
|
||||
@ -1414,13 +1482,13 @@ void signing_txack(TransactionType *tx) {
|
||||
}
|
||||
if (idx2 < tp.inputs_len - 1) {
|
||||
idx2++;
|
||||
send_req_2_prev_input();
|
||||
send_req_3_prev_input();
|
||||
} else {
|
||||
idx2 = 0;
|
||||
send_req_2_prev_output();
|
||||
send_req_3_prev_output();
|
||||
}
|
||||
return;
|
||||
case STAGE_REQUEST_2_PREV_OUTPUT:
|
||||
case STAGE_REQUEST_3_PREV_OUTPUT:
|
||||
if (!signing_validate_bin_output(&tx->bin_outputs[0])) {
|
||||
return;
|
||||
}
|
||||
@ -1434,17 +1502,12 @@ void signing_txack(TransactionType *tx) {
|
||||
return;
|
||||
}
|
||||
if (idx2 == input.prev_index) {
|
||||
if (input.has_amount && input.amount != tx->bin_outputs[0].amount) {
|
||||
if (input.amount != tx->bin_outputs[0].amount) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Invalid amount specified"));
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
if (to_spend + tx->bin_outputs[0].amount < to_spend) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError, _("Value overflow"));
|
||||
signing_abort();
|
||||
return;
|
||||
}
|
||||
#if !BITCOIN_ONLY
|
||||
if (coin->decred && tx->bin_outputs[0].decred_script_version > 0) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
@ -1454,15 +1517,14 @@ void signing_txack(TransactionType *tx) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
to_spend += tx->bin_outputs[0].amount;
|
||||
}
|
||||
if (idx2 < tp.outputs_len - 1) {
|
||||
/* Check prevtx of next input */
|
||||
idx2++;
|
||||
send_req_2_prev_output();
|
||||
send_req_3_prev_output();
|
||||
#if !BITCOIN_ONLY
|
||||
} else if (coin->extra_data && tp.extra_data_len > 0) { // has extra data
|
||||
send_req_2_prev_extradata(0, MIN(1024, tp.extra_data_len));
|
||||
send_req_3_prev_extradata(0, MIN(1024, tp.extra_data_len));
|
||||
return;
|
||||
#endif
|
||||
} else {
|
||||
@ -1473,7 +1535,7 @@ void signing_txack(TransactionType *tx) {
|
||||
}
|
||||
return;
|
||||
#if !BITCOIN_ONLY
|
||||
case STAGE_REQUEST_2_PREV_EXTRADATA:
|
||||
case STAGE_REQUEST_3_PREV_EXTRADATA:
|
||||
if (!tx_serialize_extra_data_hash(&tp, tx->extra_data.bytes,
|
||||
tx->extra_data.size)) {
|
||||
fsm_sendFailure(FailureType_Failure_ProcessError,
|
||||
@ -1482,8 +1544,8 @@ void signing_txack(TransactionType *tx) {
|
||||
return;
|
||||
}
|
||||
if (tp.extra_data_received <
|
||||
tp.extra_data_len) { // still some data remanining
|
||||
send_req_2_prev_extradata(
|
||||
tp.extra_data_len) { // still some data remaining
|
||||
send_req_3_prev_extradata(
|
||||
tp.extra_data_received,
|
||||
MIN(1024, tp.extra_data_len - tp.extra_data_received));
|
||||
} else {
|
||||
@ -1493,14 +1555,6 @@ void signing_txack(TransactionType *tx) {
|
||||
}
|
||||
return;
|
||||
#endif
|
||||
case STAGE_REQUEST_3_OUTPUT:
|
||||
if (!signing_validate_output(&tx->outputs[0]) ||
|
||||
!signing_check_output(&tx->outputs[0])) {
|
||||
return;
|
||||
}
|
||||
tx_weight += tx_output_weight(coin, &tx->outputs[0]);
|
||||
phase1_request_next_output();
|
||||
return;
|
||||
case STAGE_REQUEST_4_INPUT:
|
||||
if (!signing_validate_input(&tx->inputs[0])) {
|
||||
return;
|
||||
@ -1514,10 +1568,8 @@ void signing_txack(TransactionType *tx) {
|
||||
timestamp);
|
||||
hasher_Reset(&hasher_check);
|
||||
}
|
||||
// check prevouts and script type
|
||||
tx_prevout_hash(&hasher_check, tx->inputs);
|
||||
hasher_Update(&hasher_check, (const uint8_t *)&tx->inputs[0].script_type,
|
||||
sizeof(&tx->inputs[0].script_type));
|
||||
// check inputs are the same as those in phase 1
|
||||
tx_input_check_hash(&hasher_check, tx->inputs);
|
||||
if (idx2 == idx1) {
|
||||
if (!compile_input_script_sig(&tx->inputs[0])) {
|
||||
fsm_sendFailure(FailureType_Failure_ProcessError,
|
||||
@ -1548,7 +1600,7 @@ void signing_txack(TransactionType *tx) {
|
||||
} else {
|
||||
uint8_t hash[32] = {0};
|
||||
hasher_Final(&hasher_check, hash);
|
||||
if (memcmp(hash, hash_check, 32) != 0) {
|
||||
if (memcmp(hash, hash_inputs_check, 32) != 0) {
|
||||
fsm_sendFailure(FailureType_Failure_DataError,
|
||||
_("Transaction has changed during signing"));
|
||||
signing_abort();
|
||||
|
@ -463,6 +463,22 @@ uint32_t serialize_script_multisig(const CoinInfo *coin,
|
||||
}
|
||||
|
||||
// tx methods
|
||||
void tx_input_check_hash(Hasher *hasher, const TxInputType *input) {
|
||||
hasher_Update(hasher, input->prev_hash.bytes, sizeof(input->prev_hash.bytes));
|
||||
hasher_Update(hasher, (const uint8_t *)&input->prev_index,
|
||||
sizeof(input->prev_index));
|
||||
hasher_Update(hasher, (const uint8_t *)&input->script_type,
|
||||
sizeof(input->script_type));
|
||||
hasher_Update(hasher, (const uint8_t *)&input->address_n_count,
|
||||
sizeof(input->address_n_count));
|
||||
for (int i = 0; i < input->address_n_count; ++i)
|
||||
hasher_Update(hasher, (const uint8_t *)&input->address_n[i],
|
||||
sizeof(input->address_n[0]));
|
||||
hasher_Update(hasher, (const uint8_t *)&input->sequence,
|
||||
sizeof(input->sequence));
|
||||
hasher_Update(hasher, (const uint8_t *)&input->amount, sizeof(input->amount));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t tx_prevout_hash(Hasher *hasher, const TxInputType *input) {
|
||||
for (int i = 0; i < 32; i++) {
|
||||
@ -634,7 +650,11 @@ uint32_t tx_serialize_decred_witness(TxStruct *tx, const TxInputType *input,
|
||||
if (tx->have_inputs == 0) {
|
||||
r += ser_length(tx->inputs_len, out + r);
|
||||
}
|
||||
memcpy(out + r, &amount, 8);
|
||||
if (input->has_amount) {
|
||||
memcpy(out + r, &input->amount, 8);
|
||||
} else {
|
||||
memcpy(out + r, &amount, 8);
|
||||
}
|
||||
r += 8;
|
||||
memcpy(out + r, &block_height, 4);
|
||||
r += 4;
|
||||
|
@ -75,6 +75,7 @@ uint32_t serialize_script_multisig(const CoinInfo *coin,
|
||||
int compile_output(const CoinInfo *coin, const HDNode *root, TxOutputType *in,
|
||||
TxOutputBinType *out, bool needs_confirm);
|
||||
|
||||
void tx_input_check_hash(Hasher *hasher, const TxInputType *input);
|
||||
uint32_t tx_prevout_hash(Hasher *hasher, const TxInputType *input);
|
||||
uint32_t tx_script_hash(Hasher *hasher, uint32_t size, const uint8_t *data);
|
||||
uint32_t tx_sequence_hash(Hasher *hasher, const TxInputType *input);
|
||||
|
Loading…
Reference in New Issue
Block a user