add test for batch CS derive/sign logic
This commit is contained in:
parent
390d241019
commit
231cdaf4f7
@ -512,6 +512,28 @@ fail_sign (struct TES_Client *client,
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate error response that deriving failed.
|
||||||
|
*
|
||||||
|
* @param client client to send response to
|
||||||
|
* @param ec error code to include
|
||||||
|
* @return #GNUNET_OK on success
|
||||||
|
*/
|
||||||
|
static enum GNUNET_GenericReturnValue
|
||||||
|
fail_derive (struct TES_Client *client,
|
||||||
|
enum TALER_ErrorCode ec)
|
||||||
|
{
|
||||||
|
struct TALER_CRYPTO_RDeriveFailure sf = {
|
||||||
|
.header.size = htons (sizeof (sf)),
|
||||||
|
.header.type = htons (TALER_HELPER_CS_MT_RES_RDERIVE_FAILURE),
|
||||||
|
.ec = htonl (ec)
|
||||||
|
};
|
||||||
|
|
||||||
|
return TES_transmit (client->csock,
|
||||||
|
&sf.header);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate signature response.
|
* Generate signature response.
|
||||||
*
|
*
|
||||||
@ -842,19 +864,25 @@ finish_job (struct TES_Client *client,
|
|||||||
{
|
{
|
||||||
sem_down (&bj->sem);
|
sem_down (&bj->sem);
|
||||||
sem_done (&bj->sem);
|
sem_done (&bj->sem);
|
||||||
if (TALER_EC_NONE != bj->ec)
|
|
||||||
{
|
|
||||||
fail_sign (client,
|
|
||||||
bj->ec);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (bj->type)
|
switch (bj->type)
|
||||||
{
|
{
|
||||||
case TYPE_SIGN:
|
case TYPE_SIGN:
|
||||||
|
if (TALER_EC_NONE != bj->ec)
|
||||||
|
{
|
||||||
|
fail_sign (client,
|
||||||
|
bj->ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
send_signature (client,
|
send_signature (client,
|
||||||
&bj->details.sign.cs_answer);
|
&bj->details.sign.cs_answer);
|
||||||
break;
|
break;
|
||||||
case TYPE_RDERIVE:
|
case TYPE_RDERIVE:
|
||||||
|
if (TALER_EC_NONE != bj->ec)
|
||||||
|
{
|
||||||
|
fail_derive (client,
|
||||||
|
bj->ec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
send_derivation (client,
|
send_derivation (client,
|
||||||
&bj->details.rderive.rpairp);
|
&bj->details.rderive.rpairp);
|
||||||
break;
|
break;
|
||||||
@ -878,16 +906,19 @@ handle_batch_sign_request (struct TES_Client *client,
|
|||||||
uint16_t size = ntohs (bsr->header.size) - sizeof (*bsr);
|
uint16_t size = ntohs (bsr->header.size) - sizeof (*bsr);
|
||||||
const void *off = (const void *) &bsr[1];
|
const void *off = (const void *) &bsr[1];
|
||||||
unsigned int idx = 0;
|
unsigned int idx = 0;
|
||||||
struct BatchJob jobs[bs];
|
struct BatchJob jobs[GNUNET_NZL (bs)];
|
||||||
bool failure = false;
|
bool failure = false;
|
||||||
|
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
|
||||||
|
"Handling batch sign request of size %u\n",
|
||||||
|
(unsigned int) bs);
|
||||||
if (bs > TALER_MAX_FRESH_COINS)
|
if (bs > TALER_MAX_FRESH_COINS)
|
||||||
{
|
{
|
||||||
GNUNET_break_op (0);
|
GNUNET_break_op (0);
|
||||||
return GNUNET_SYSERR;
|
return GNUNET_SYSERR;
|
||||||
}
|
}
|
||||||
while ( (bs > 0) &&
|
while ( (bs > 0) &&
|
||||||
(size > sizeof (struct TALER_CRYPTO_CsSignRequestMessage)) )
|
(size >= sizeof (struct TALER_CRYPTO_CsSignRequestMessage)) )
|
||||||
{
|
{
|
||||||
const struct TALER_CRYPTO_CsSignRequestMessage *sr = off;
|
const struct TALER_CRYPTO_CsSignRequestMessage *sr = off;
|
||||||
uint16_t s = ntohs (sr->header.size);
|
uint16_t s = ntohs (sr->header.size);
|
||||||
@ -903,6 +934,9 @@ handle_batch_sign_request (struct TES_Client *client,
|
|||||||
off += s;
|
off += s;
|
||||||
size -= s;
|
size -= s;
|
||||||
}
|
}
|
||||||
|
GNUNET_break_op (0 == size);
|
||||||
|
bs = GNUNET_MIN (bs,
|
||||||
|
idx);
|
||||||
for (unsigned int i = 0; i<bs; i++)
|
for (unsigned int i = 0; i<bs; i++)
|
||||||
finish_job (client,
|
finish_job (client,
|
||||||
&jobs[i]);
|
&jobs[i]);
|
||||||
@ -941,13 +975,16 @@ handle_batch_derive_request (struct TES_Client *client,
|
|||||||
struct BatchJob jobs[bs];
|
struct BatchJob jobs[bs];
|
||||||
bool failure = false;
|
bool failure = false;
|
||||||
|
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
|
||||||
|
"Handling batch derivation request of size %u\n",
|
||||||
|
(unsigned int) bs);
|
||||||
if (bs > TALER_MAX_FRESH_COINS)
|
if (bs > TALER_MAX_FRESH_COINS)
|
||||||
{
|
{
|
||||||
GNUNET_break_op (0);
|
GNUNET_break_op (0);
|
||||||
return GNUNET_SYSERR;
|
return GNUNET_SYSERR;
|
||||||
}
|
}
|
||||||
while ( (bs > 0) &&
|
while ( (bs > 0) &&
|
||||||
(size > sizeof (struct TALER_CRYPTO_CsRDeriveRequest)) )
|
(size >= sizeof (struct TALER_CRYPTO_CsRDeriveRequest)) )
|
||||||
{
|
{
|
||||||
const struct TALER_CRYPTO_CsRDeriveRequest *rdr = off;
|
const struct TALER_CRYPTO_CsRDeriveRequest *rdr = off;
|
||||||
uint16_t s = ntohs (rdr->header.size);
|
uint16_t s = ntohs (rdr->header.size);
|
||||||
@ -964,20 +1001,17 @@ handle_batch_derive_request (struct TES_Client *client,
|
|||||||
off += s;
|
off += s;
|
||||||
size -= s;
|
size -= s;
|
||||||
}
|
}
|
||||||
|
GNUNET_break_op (0 == size);
|
||||||
|
bs = GNUNET_MIN (bs,
|
||||||
|
idx);
|
||||||
for (unsigned int i = 0; i<bs; i++)
|
for (unsigned int i = 0; i<bs; i++)
|
||||||
finish_job (client,
|
finish_job (client,
|
||||||
&jobs[i]);
|
&jobs[i]);
|
||||||
if (failure)
|
if (failure)
|
||||||
{
|
{
|
||||||
struct TALER_CRYPTO_SignFailure sf = {
|
|
||||||
.header.size = htons (sizeof (sf)),
|
|
||||||
.header.type = htons (TALER_HELPER_CS_MT_RES_BATCH_RDERIVE_FAILURE),
|
|
||||||
.ec = htonl (TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE)
|
|
||||||
};
|
|
||||||
|
|
||||||
GNUNET_break (0);
|
GNUNET_break (0);
|
||||||
return TES_transmit (client->csock,
|
return fail_derive (client,
|
||||||
&sf.header);
|
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE);
|
||||||
}
|
}
|
||||||
return GNUNET_OK;
|
return GNUNET_OK;
|
||||||
}
|
}
|
||||||
@ -1219,14 +1253,8 @@ handle_r_derive_request (struct TES_Client *client,
|
|||||||
&r_pub);
|
&r_pub);
|
||||||
if (TALER_EC_NONE != ec)
|
if (TALER_EC_NONE != ec)
|
||||||
{
|
{
|
||||||
struct TALER_CRYPTO_RDeriveFailure rdf = {
|
return fail_derive (client,
|
||||||
.header.size = htons (sizeof (rdf)),
|
ec);
|
||||||
.header.type = htons (TALER_HELPER_CS_MT_RES_RDERIVE_FAILURE),
|
|
||||||
.ec = htonl (ec)
|
|
||||||
};
|
|
||||||
|
|
||||||
return TES_transmit (client->csock,
|
|
||||||
&rdf.header);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = send_derivation (client,
|
ret = send_derivation (client,
|
||||||
|
@ -766,6 +766,9 @@ handle_batch_sign_request (struct TES_Client *client,
|
|||||||
off += s;
|
off += s;
|
||||||
size -= s;
|
size -= s;
|
||||||
}
|
}
|
||||||
|
GNUNET_break_op (0 == size);
|
||||||
|
bs = GNUNET_MIN (bs,
|
||||||
|
idx);
|
||||||
for (unsigned int i = 0; i<bs; i++)
|
for (unsigned int i = 0; i<bs; i++)
|
||||||
finish_job (client,
|
finish_job (client,
|
||||||
&jobs[i]);
|
&jobs[i]);
|
||||||
|
@ -439,8 +439,6 @@ test_signing (struct TALER_CRYPTO_CsDenominationHelper *dh)
|
|||||||
};
|
};
|
||||||
|
|
||||||
pd.blinded_planchet.cipher = TALER_DENOMINATION_CS;
|
pd.blinded_planchet.cipher = TALER_DENOMINATION_CS;
|
||||||
// keys[i].denom_pub.cipher = TALER_DENOMINATION_CS;
|
|
||||||
|
|
||||||
TALER_cs_withdraw_nonce_derive (&ps,
|
TALER_cs_withdraw_nonce_derive (&ps,
|
||||||
&pd.blinded_planchet.details.
|
&pd.blinded_planchet.details.
|
||||||
cs_blinded_planchet.nonce);
|
cs_blinded_planchet.nonce);
|
||||||
@ -592,6 +590,209 @@ test_signing (struct TALER_CRYPTO_CsDenominationHelper *dh)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test batch signing logic.
|
||||||
|
*
|
||||||
|
* @param dh handle to the helper
|
||||||
|
* @param batch_size how large should the batch be
|
||||||
|
* @param check_sigs also check unknown key and signatures
|
||||||
|
* @return 0 on success
|
||||||
|
*/
|
||||||
|
static int
|
||||||
|
test_batch_signing (struct TALER_CRYPTO_CsDenominationHelper *dh,
|
||||||
|
unsigned int batch_size,
|
||||||
|
bool check_sigs)
|
||||||
|
{
|
||||||
|
struct TALER_BlindedDenominationSignature ds[batch_size];
|
||||||
|
enum TALER_ErrorCode ec;
|
||||||
|
bool success = false;
|
||||||
|
struct TALER_PlanchetMasterSecretP ps[batch_size];
|
||||||
|
struct TALER_CoinSpendPrivateKeyP coin_priv[batch_size];
|
||||||
|
union TALER_DenominationBlindingKeyP bks[batch_size];
|
||||||
|
struct TALER_CoinPubHashP c_hash[batch_size];
|
||||||
|
struct TALER_ExchangeWithdrawValues alg_values[batch_size];
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i<batch_size; i++)
|
||||||
|
TALER_planchet_master_setup_random (&ps[i]);
|
||||||
|
for (unsigned int k = 0; k<MAX_KEYS; k++)
|
||||||
|
{
|
||||||
|
if (! keys[k].valid)
|
||||||
|
continue;
|
||||||
|
{
|
||||||
|
struct TALER_PlanchetDetail pd[batch_size];
|
||||||
|
struct TALER_CRYPTO_CsSignRequest csr[batch_size];
|
||||||
|
struct TALER_CRYPTO_CsDeriveRequest cdr[batch_size];
|
||||||
|
struct TALER_DenominationCSPublicRPairP crps[batch_size];
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i<batch_size; i++)
|
||||||
|
{
|
||||||
|
cdr[i].h_cs = &keys[k].h_cs;
|
||||||
|
cdr[i].nonce =
|
||||||
|
&pd[i].blinded_planchet.details.cs_blinded_planchet.nonce;
|
||||||
|
pd[i].blinded_planchet.cipher = TALER_DENOMINATION_CS;
|
||||||
|
TALER_cs_withdraw_nonce_derive (
|
||||||
|
&ps[i],
|
||||||
|
&pd[i].blinded_planchet.details.cs_blinded_planchet.nonce);
|
||||||
|
alg_values[i].cipher = TALER_DENOMINATION_CS;
|
||||||
|
}
|
||||||
|
ec = TALER_CRYPTO_helper_cs_r_batch_derive_withdraw (
|
||||||
|
dh,
|
||||||
|
cdr,
|
||||||
|
batch_size,
|
||||||
|
crps);
|
||||||
|
if (TALER_EC_NONE != ec)
|
||||||
|
continue;
|
||||||
|
for (unsigned int i = 0; i<batch_size; i++)
|
||||||
|
{
|
||||||
|
alg_values[i].details.cs_values = crps[i];
|
||||||
|
TALER_planchet_setup_coin_priv (&ps[i],
|
||||||
|
&alg_values[i],
|
||||||
|
&coin_priv[i]);
|
||||||
|
TALER_planchet_blinding_secret_create (&ps[i],
|
||||||
|
&alg_values[i],
|
||||||
|
&bks[i]);
|
||||||
|
GNUNET_assert (GNUNET_YES ==
|
||||||
|
TALER_planchet_prepare (&keys[k].denom_pub,
|
||||||
|
&alg_values[i],
|
||||||
|
&bks[i],
|
||||||
|
&coin_priv[i],
|
||||||
|
NULL, /* no age commitment */
|
||||||
|
&c_hash[i],
|
||||||
|
&pd[i]));
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
|
||||||
|
"Requesting signature with key %s\n",
|
||||||
|
GNUNET_h2s (&keys[k].h_cs.hash));
|
||||||
|
csr[i].h_cs = &keys[k].h_cs;
|
||||||
|
csr[i].blinded_planchet
|
||||||
|
= &pd[i].blinded_planchet.details.cs_blinded_planchet;
|
||||||
|
}
|
||||||
|
ec = TALER_CRYPTO_helper_cs_batch_sign_withdraw (
|
||||||
|
dh,
|
||||||
|
csr,
|
||||||
|
batch_size,
|
||||||
|
ds);
|
||||||
|
}
|
||||||
|
switch (ec)
|
||||||
|
{
|
||||||
|
case TALER_EC_NONE:
|
||||||
|
if (GNUNET_TIME_relative_cmp (GNUNET_TIME_absolute_get_remaining (
|
||||||
|
keys[k].start_time.abs_time),
|
||||||
|
>,
|
||||||
|
GNUNET_TIME_UNIT_SECONDS))
|
||||||
|
{
|
||||||
|
/* key worked too early */
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (GNUNET_TIME_relative_cmp (GNUNET_TIME_absolute_get_duration (
|
||||||
|
keys[k].start_time.abs_time),
|
||||||
|
>,
|
||||||
|
keys[k].validity_duration))
|
||||||
|
{
|
||||||
|
/* key worked too later */
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
if (check_sigs)
|
||||||
|
{
|
||||||
|
for (unsigned int i = 0; i<batch_size; i++)
|
||||||
|
{
|
||||||
|
struct TALER_FreshCoin coin;
|
||||||
|
|
||||||
|
if (GNUNET_OK !=
|
||||||
|
TALER_planchet_to_coin (&keys[k].denom_pub,
|
||||||
|
&ds[i],
|
||||||
|
&bks[i],
|
||||||
|
&coin_priv[i],
|
||||||
|
NULL, /* no age commitment */
|
||||||
|
&c_hash[i],
|
||||||
|
&alg_values[i],
|
||||||
|
&coin))
|
||||||
|
{
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
|
||||||
|
"Received valid signature for key %s\n",
|
||||||
|
GNUNET_h2s (&keys[k].h_cs.hash));
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
case TALER_EC_EXCHANGE_DENOMINATION_HELPER_TOO_EARLY:
|
||||||
|
/* This 'failure' is expected, we're testing also for the
|
||||||
|
error handling! */
|
||||||
|
if ( (GNUNET_TIME_relative_is_zero (
|
||||||
|
GNUNET_TIME_absolute_get_remaining (
|
||||||
|
keys[k].start_time.abs_time))) &&
|
||||||
|
(GNUNET_TIME_relative_cmp (
|
||||||
|
GNUNET_TIME_absolute_get_duration (
|
||||||
|
keys[k].start_time.abs_time),
|
||||||
|
<,
|
||||||
|
keys[k].validity_duration)) )
|
||||||
|
{
|
||||||
|
/* key should have worked! */
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
/* unexpected error */
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
|
||||||
|
"Unexpected error %d\n",
|
||||||
|
ec);
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! success)
|
||||||
|
{
|
||||||
|
/* no valid key for signing found, also bad */
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check signing does not work if the key is unknown */
|
||||||
|
if (check_sigs)
|
||||||
|
{
|
||||||
|
struct TALER_PlanchetDetail pd;
|
||||||
|
struct TALER_CsPubHashP rnd;
|
||||||
|
struct TALER_CRYPTO_CsSignRequest csr;
|
||||||
|
|
||||||
|
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
|
||||||
|
&rnd,
|
||||||
|
sizeof (rnd));
|
||||||
|
pd.blinded_planchet.cipher = TALER_DENOMINATION_CS;
|
||||||
|
GNUNET_assert (GNUNET_YES ==
|
||||||
|
TALER_planchet_prepare (&keys[0].denom_pub,
|
||||||
|
&alg_values[0],
|
||||||
|
&bks[0],
|
||||||
|
&coin_priv[0],
|
||||||
|
NULL, /* no age commitment */
|
||||||
|
&c_hash[0],
|
||||||
|
&pd));
|
||||||
|
csr.h_cs = &rnd;
|
||||||
|
csr.blinded_planchet
|
||||||
|
= &pd.blinded_planchet.details.cs_blinded_planchet;
|
||||||
|
ec = TALER_CRYPTO_helper_cs_batch_sign_withdraw (
|
||||||
|
dh,
|
||||||
|
&csr,
|
||||||
|
1,
|
||||||
|
&ds[0]);
|
||||||
|
if (TALER_EC_EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN != ec)
|
||||||
|
{
|
||||||
|
if (TALER_EC_NONE == ec)
|
||||||
|
TALER_blinded_denom_sig_free (ds);
|
||||||
|
GNUNET_break (0);
|
||||||
|
return 17;
|
||||||
|
}
|
||||||
|
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
|
||||||
|
"Signing with invalid key %s failed as desired\n",
|
||||||
|
GNUNET_h2s (&rnd.hash));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benchmark signing logic.
|
* Benchmark signing logic.
|
||||||
*
|
*
|
||||||
@ -812,6 +1013,34 @@ run_test (void)
|
|||||||
ret = test_r_derive (dh);
|
ret = test_r_derive (dh);
|
||||||
if (0 == ret)
|
if (0 == ret)
|
||||||
ret = test_signing (dh);
|
ret = test_signing (dh);
|
||||||
|
if (0 == ret)
|
||||||
|
ret = test_batch_signing (dh,
|
||||||
|
2,
|
||||||
|
true);
|
||||||
|
if (0 == ret)
|
||||||
|
ret = test_batch_signing (dh,
|
||||||
|
256,
|
||||||
|
true);
|
||||||
|
for (unsigned int i = 0; i<5; i++)
|
||||||
|
{
|
||||||
|
static unsigned int batches[] = { 1, 4, 16, 64, 256 };
|
||||||
|
unsigned int batch_size = batches[i];
|
||||||
|
struct GNUNET_TIME_Absolute start;
|
||||||
|
struct GNUNET_TIME_Relative duration;
|
||||||
|
|
||||||
|
start = GNUNET_TIME_absolute_get ();
|
||||||
|
if (0 != ret)
|
||||||
|
break;
|
||||||
|
ret = test_batch_signing (dh,
|
||||||
|
batch_size,
|
||||||
|
false);
|
||||||
|
duration = GNUNET_TIME_absolute_get_duration (start);
|
||||||
|
fprintf (stderr,
|
||||||
|
"%4u (batch) signature operations took %s (total real time)\n",
|
||||||
|
(unsigned int) batch_size,
|
||||||
|
GNUNET_STRINGS_relative_time_to_string (duration,
|
||||||
|
GNUNET_YES));
|
||||||
|
}
|
||||||
if (0 == ret)
|
if (0 == ret)
|
||||||
ret = perf_signing (dh,
|
ret = perf_signing (dh,
|
||||||
"sequential");
|
"sequential");
|
||||||
@ -835,13 +1064,14 @@ main (int argc,
|
|||||||
int ret;
|
int ret;
|
||||||
enum GNUNET_OS_ProcessStatusType type;
|
enum GNUNET_OS_ProcessStatusType type;
|
||||||
unsigned long code;
|
unsigned long code;
|
||||||
|
const char *loglev = "WARNING";
|
||||||
|
|
||||||
(void) argc;
|
(void) argc;
|
||||||
(void) argv;
|
(void) argv;
|
||||||
unsetenv ("XDG_DATA_HOME");
|
unsetenv ("XDG_DATA_HOME");
|
||||||
unsetenv ("XDG_CONFIG_HOME");
|
unsetenv ("XDG_CONFIG_HOME");
|
||||||
GNUNET_log_setup ("test-helper-cs",
|
GNUNET_log_setup ("test-helper-cs",
|
||||||
"WARNING",
|
loglev,
|
||||||
NULL);
|
NULL);
|
||||||
GNUNET_OS_init (TALER_project_data_default ());
|
GNUNET_OS_init (TALER_project_data_default ());
|
||||||
libexec_dir = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_BINDIR);
|
libexec_dir = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_BINDIR);
|
||||||
@ -857,7 +1087,7 @@ main (int argc,
|
|||||||
"-c",
|
"-c",
|
||||||
"test_helper_cs.conf",
|
"test_helper_cs.conf",
|
||||||
"-L",
|
"-L",
|
||||||
"WARNING",
|
loglev,
|
||||||
NULL);
|
NULL);
|
||||||
if (NULL == helper)
|
if (NULL == helper)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user