WiP: age-withdraw implementation, part 1/n
Commit phase of the age-withdraw protocol implemented, according to https://docs.taler.net/core/api-exchange.html#withdraw-with-age-restriction
This commit is contained in:
parent
7f518fff1a
commit
b4128c2c2a
@ -128,6 +128,8 @@ taler_exchange_httpd_SOURCES = \
|
||||
taler-exchange-httpd_aml-decisions-get.c \
|
||||
taler-exchange-httpd_batch-deposit.c taler-exchange-httpd_batch-deposit.h \
|
||||
taler-exchange-httpd_batch-withdraw.c taler-exchange-httpd_batch-withdraw.h \
|
||||
taler-exchange-httpd_age-withdraw.c taler-exchange-httpd_age-withdraw.h \
|
||||
taler-exchange-httpd_age-withdraw_reveal.c taler-exchange-httpd_age-withdraw_reveal.h \
|
||||
taler-exchange-httpd_common_deposit.c taler-exchange-httpd_common_deposit.h \
|
||||
taler-exchange-httpd_config.c taler-exchange-httpd_config.h \
|
||||
taler-exchange-httpd_contract.c taler-exchange-httpd_contract.h \
|
||||
|
@ -163,7 +163,7 @@ TEH_handler_aml_decision_get (
|
||||
json_t *aml_history;
|
||||
json_t *kyc_attributes;
|
||||
enum GNUNET_DB_QueryStatus qs;
|
||||
bool none;
|
||||
bool none = false;
|
||||
|
||||
aml_history = json_array ();
|
||||
GNUNET_assert (NULL != aml_history);
|
||||
|
@ -34,18 +34,20 @@ enum TEH_MetricTypeRequest
|
||||
TEH_MT_REQUEST_OTHER = 0,
|
||||
TEH_MT_REQUEST_DEPOSIT = 1,
|
||||
TEH_MT_REQUEST_WITHDRAW = 2,
|
||||
TEH_MT_REQUEST_MELT = 3,
|
||||
TEH_MT_REQUEST_PURSE_CREATE = 4,
|
||||
TEH_MT_REQUEST_PURSE_MERGE = 5,
|
||||
TEH_MT_REQUEST_RESERVE_PURSE = 6,
|
||||
TEH_MT_REQUEST_PURSE_DEPOSIT = 7,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 8,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 9,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_MELT = 10,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW = 11,
|
||||
TEH_MT_REQUEST_BATCH_DEPOSIT = 12,
|
||||
TEH_MT_REQUEST_POLICY_FULFILLMENT = 13,
|
||||
TEH_MT_REQUEST_COUNT = 14 /* MUST BE LAST! */
|
||||
TEH_MT_REQUEST_AGE_WITHDRAW = 3,
|
||||
TEH_MT_REQUEST_MELT = 4,
|
||||
TEH_MT_REQUEST_PURSE_CREATE = 5,
|
||||
TEH_MT_REQUEST_PURSE_MERGE = 6,
|
||||
TEH_MT_REQUEST_RESERVE_PURSE = 7,
|
||||
TEH_MT_REQUEST_PURSE_DEPOSIT = 8,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_DEPOSIT = 9,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_WITHDRAW = 10,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_AGE_WITHDRAW = 11,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_MELT = 12,
|
||||
TEH_MT_REQUEST_IDEMPOTENT_BATCH_WITHDRAW = 13,
|
||||
TEH_MT_REQUEST_BATCH_DEPOSIT = 14,
|
||||
TEH_MT_REQUEST_POLICY_FULFILLMENT = 15,
|
||||
TEH_MT_REQUEST_COUNT = 16 /* MUST BE LAST! */
|
||||
};
|
||||
|
||||
/**
|
||||
@ -55,10 +57,11 @@ enum TEH_MetricTypeSuccess
|
||||
{
|
||||
TEH_MT_SUCCESS_DEPOSIT = 0,
|
||||
TEH_MT_SUCCESS_WITHDRAW = 1,
|
||||
TEH_MT_SUCCESS_BATCH_WITHDRAW = 2,
|
||||
TEH_MT_SUCCESS_MELT = 3,
|
||||
TEH_MT_SUCCESS_REFRESH_REVEAL = 4,
|
||||
TEH_MT_SUCCESS_COUNT = 5 /* MUST BE LAST! */
|
||||
TEH_MT_SUCCESS_AGE_WITHDRAW = 2,
|
||||
TEH_MT_SUCCESS_BATCH_WITHDRAW = 3,
|
||||
TEH_MT_SUCCESS_MELT = 4,
|
||||
TEH_MT_SUCCESS_REFRESH_REVEAL = 5,
|
||||
TEH_MT_SUCCESS_COUNT = 6 /* MUST BE LAST! */
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -115,6 +115,7 @@ libtaler_plugin_exchangedb_postgres_la_SOURCES = \
|
||||
pg_drain_kyc_alert.h pg_drain_kyc_alert.c \
|
||||
pg_reserves_in_insert.h pg_reserves_in_insert.c \
|
||||
pg_get_withdraw_info.h pg_get_withdraw_info.c \
|
||||
pg_get_age_withdraw_info.c pg_get_age_withdraw_info.h \
|
||||
pg_do_batch_withdraw.h pg_do_batch_withdraw.c \
|
||||
pg_get_policy_details.h pg_get_policy_details.c \
|
||||
pg_persist_policy_details.h pg_persist_policy_details.c \
|
||||
|
@ -116,6 +116,7 @@
|
||||
#include "pg_drain_kyc_alert.h"
|
||||
#include "pg_reserves_in_insert.h"
|
||||
#include "pg_get_withdraw_info.h"
|
||||
#include "pg_get_age_withdraw_info.h"
|
||||
#include "pg_do_batch_withdraw.h"
|
||||
#include "pg_get_policy_details.h"
|
||||
#include "pg_persist_policy_details.h"
|
||||
@ -580,6 +581,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
|
||||
= &TEH_PG_get_withdraw_info;
|
||||
plugin->do_batch_withdraw
|
||||
= &TEH_PG_do_batch_withdraw;
|
||||
plugin->get_age_withdraw_info
|
||||
= &TEH_PG_get_age_withdraw_info;
|
||||
plugin->get_policy_details
|
||||
= &TEH_PG_get_policy_details;
|
||||
plugin->persist_policy_details
|
||||
|
@ -46,6 +46,7 @@
|
||||
* fixed and part of the protocol.
|
||||
*/
|
||||
#define TALER_CNC_KAPPA 3
|
||||
#define TALER_CNC_KAPPA_MINUS_ONE_STR "2"
|
||||
|
||||
|
||||
/* ****************** Coin crypto primitives ************* */
|
||||
@ -436,6 +437,15 @@ struct TALER_AgeCommitmentPublicKeyP
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* @brief Hash to represent the commitment to n*kappa blinded keys during a age-withdrawal.
|
||||
*/
|
||||
struct TALER_AgeWithdrawCommitmentHashP
|
||||
{
|
||||
struct GNUNET_HashCode hash;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Type of online public keys used by the wallet to establish a purse and the associated contract meta data.
|
||||
*/
|
||||
@ -3700,6 +3710,42 @@ TALER_wallet_withdraw_verify (
|
||||
const struct TALER_ReserveSignatureP *reserve_sig);
|
||||
|
||||
|
||||
/**
|
||||
* Sign age-withdraw request.
|
||||
*
|
||||
* @param h_commitment hash all n*kappa blinded coins in the commitment for the age-withdraw
|
||||
* @param amount_with_fee amount to debit the reserve for
|
||||
* @param max_age_group maximum age group that the withdrawn coins must be restricted to
|
||||
* @param reserve_priv private key to sign with
|
||||
* @param[out] reserve_sig resulting signature
|
||||
*/
|
||||
void
|
||||
TALER_wallet_age_withdraw_sign (
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
const struct TALER_Amount *amount_with_fee,
|
||||
uint32_t max_age_group,
|
||||
const struct TALER_ReservePrivateKeyP *reserve_priv,
|
||||
struct TALER_ReserveSignatureP *reserve_sig);
|
||||
|
||||
/**
|
||||
* Verify an age-withdraw request.
|
||||
*
|
||||
* @param h_commitment hash all n*kappa blinded coins in the commitment for the age-withdraw
|
||||
* @param amount_with_fee amount to debit the reserve for
|
||||
* @param max_age_group maximum age group that the withdrawn coins must be restricted to
|
||||
* @param reserve_pub public key of the reserve
|
||||
* @param reserve_sig resulting signature
|
||||
* @return #GNUNET_OK if the signature is valid
|
||||
*/
|
||||
enum GNUNET_GenericReturnValue
|
||||
TALER_wallet_age_withdraw_verify (
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
const struct TALER_Amount *amount_with_fee,
|
||||
uint32_t max_age_group,
|
||||
const struct TALER_ReservePublicKeyP *reserve_pub,
|
||||
const struct TALER_ReserveSignatureP *reserve_sig);
|
||||
|
||||
|
||||
/**
|
||||
* Verify exchange melt confirmation.
|
||||
*
|
||||
@ -4789,6 +4835,25 @@ TALER_exchange_online_purse_status_verify (
|
||||
const struct TALER_ExchangeSignatureP *exchange_sig);
|
||||
|
||||
|
||||
/**
|
||||
* Create age-withdraw confirmation signature.
|
||||
*
|
||||
* @param scb function to call to create the signature
|
||||
* @param awch age-withdraw commitment that identifies the n*kappa blinded coins
|
||||
* @param noreveal_index gamma cut-and-choose value chosen by the exchange
|
||||
* @param[out] pub where to write the exchange public key
|
||||
* @param[out] sig where to write the exchange signature
|
||||
* @return #TALER_EC_NONE on success
|
||||
*/
|
||||
enum TALER_ErrorCode
|
||||
TALER_exchange_online_age_withdraw_confirmation_sign (
|
||||
TALER_ExchangeSignCallback scb,
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
uint32_t noreveal_index,
|
||||
struct TALER_ExchangePublicKeyP *pub,
|
||||
struct TALER_ExchangeSignatureP *sig);
|
||||
|
||||
|
||||
/* ********************* offline signing ************************** */
|
||||
|
||||
|
||||
|
@ -1051,6 +1051,58 @@ struct TALER_EXCHANGEDB_CollectableBlindcoin
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Information we keep for an age-withdraw commitment
|
||||
* to reproduce the /age-withdraw operation if neede, and to have proof
|
||||
* that a reserve was drained by this amount.
|
||||
*/
|
||||
struct TALER_EXCHANGEDB_AgeWithdrawCommitment
|
||||
{
|
||||
/**
|
||||
* Total amount (with fee) committed to withdraw
|
||||
*/
|
||||
struct TALER_Amount amount_with_fee;
|
||||
|
||||
/**
|
||||
* Maximum age group that the coins are restricted to.
|
||||
*/
|
||||
uint32_t max_age_group;
|
||||
|
||||
/**
|
||||
* The hash of the commitment of all n*kappa coins
|
||||
*/
|
||||
struct TALER_AgeWithdrawCommitmentHashP h_commitment;
|
||||
|
||||
/**
|
||||
* Index (smaller #TALER_CNC_KAPPA) which the exchange has chosen to not have
|
||||
* revealed during cut and choose. This value applies to all n coins in the
|
||||
* commitment.
|
||||
*/
|
||||
uint32_t noreveal_index;
|
||||
|
||||
/**
|
||||
* Public key of the reserve that was drained.
|
||||
*/
|
||||
struct TALER_ReservePublicKeyP reserve_pub;
|
||||
|
||||
/**
|
||||
* Signature confirming the age withdrawal, matching @e reserve_pub, @e
|
||||
* maximum_age_group and @e h_commitment and @e total_amount_with_fee.
|
||||
*/
|
||||
struct TALER_ReserveSignatureP reserve_sig;
|
||||
|
||||
/**
|
||||
* The exchange's signature of the response.
|
||||
*/
|
||||
struct TALER_ExchangeSignatureP sig;
|
||||
|
||||
/**
|
||||
* Timestamp of the request beeing made
|
||||
*/
|
||||
struct GNUNET_TIME_Timestamp timestamp;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Information the exchange records about a recoup request
|
||||
* in a reserve history.
|
||||
@ -3607,6 +3659,46 @@ struct TALER_EXCHANGEDB_Plugin
|
||||
bool *conflict,
|
||||
bool *nonce_reuse);
|
||||
|
||||
/**
|
||||
* Locate the response for a age-withdraw request under a hash that uniquely
|
||||
* identifies the age-withdraw operation. Used to ensure idempotency of the
|
||||
* request.
|
||||
*
|
||||
* @param cls the @e cls of this struct with the plugin-specific state
|
||||
* @param reserve_pub public key of the reserve for which the age-withdraw request is made
|
||||
* @param ach hash that uniquely identifies the age-withdraw operation
|
||||
* @param[out] awc corresponding details of the previous age-withdraw request if an entry was found
|
||||
* @return statement execution status
|
||||
*/
|
||||
enum GNUNET_DB_QueryStatus
|
||||
(*get_age_withdraw_info)(
|
||||
void *cls,
|
||||
const struct TALER_ReservePublicKeyP *reserve_pub,
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *ach,
|
||||
struct TALER_EXCHANGEDB_AgeWithdrawCommitment *awc);
|
||||
|
||||
/**
|
||||
* Perform an age-withdraw operation, checking for sufficient balance
|
||||
* and possibly persisting the withdrawal details.
|
||||
*
|
||||
* @param cls the `struct PostgresClosure` with the plugin-specific state
|
||||
* @param commitment corresponding commitment for the age-withdraw
|
||||
* @param now current time (rounded)
|
||||
* @param[out] found set to true if the reserve was found
|
||||
* @param[out] balance_ok set to true if the balance was sufficient
|
||||
* @param[out] ruuid set to the reserve's UUID (reserves table row)
|
||||
* @return query execution status
|
||||
*/
|
||||
enum GNUNET_DB_QueryStatus
|
||||
(*do_age_withdraw)(
|
||||
void *cls,
|
||||
const struct TALER_EXCHANGEDB_AgeWithdrawCommitment *commitment,
|
||||
struct GNUNET_TIME_Timestamp now,
|
||||
bool *found,
|
||||
bool *balance_ok,
|
||||
uint64_t *ruuid);
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the details to a policy given by its hash_code
|
||||
*
|
||||
|
@ -73,8 +73,12 @@ enum TALER_KYCLOGIC_KycTriggerEvent
|
||||
/**
|
||||
* Reserve is being closed by force.
|
||||
*/
|
||||
TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE = 4
|
||||
TALER_KYCLOGIC_KYC_TRIGGER_RESERVE_CLOSE = 4,
|
||||
|
||||
/**
|
||||
* Customer withdraws coins via age-withdraw.
|
||||
*/
|
||||
TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW = 5,
|
||||
};
|
||||
|
||||
|
||||
|
@ -186,6 +186,7 @@ TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s,
|
||||
enum TALER_KYCLOGIC_KycTriggerEvent out;
|
||||
} map [] = {
|
||||
{ "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW },
|
||||
{ "age-withdraw", TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW },
|
||||
{ "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT },
|
||||
{ "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE },
|
||||
{ "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE },
|
||||
@ -214,6 +215,8 @@ TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger)
|
||||
{
|
||||
case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW:
|
||||
return "withdraw";
|
||||
case TALER_KYCLOGIC_KYC_TRIGGER_AGE_WITHDRAW:
|
||||
return "age-withdraw";
|
||||
case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT:
|
||||
return "deposit";
|
||||
case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE:
|
||||
|
@ -359,6 +359,63 @@ TALER_exchange_online_melt_confirmation_verify (
|
||||
}
|
||||
|
||||
|
||||
GNUNET_NETWORK_STRUCT_BEGIN
|
||||
|
||||
/**
|
||||
* @brief Format of the block signed by the Exchange in response to a
|
||||
* successful "/reserves/$RESERVE_PUB/age-withdraw" request. Hereby the
|
||||
* exchange affirms that the commitment along with the maximum age group and
|
||||
* the amount were accepted. This also commits the exchange to a particular
|
||||
* index to not be revealed during the reveal.
|
||||
*/
|
||||
struct TALER_AgeWithdrawConfirmationPS
|
||||
{
|
||||
/**
|
||||
* Purpose is #TALER_SIGNATURE_EXCHANGE_CONFIRM_AGE_WITHDRAW. Signed by a
|
||||
* `struct TALER_ExchangePublicKeyP` using EdDSA.
|
||||
*/
|
||||
struct GNUNET_CRYPTO_EccSignaturePurpose purpose;
|
||||
|
||||
/**
|
||||
* Commitment made in the /reserves/$RESERVE_PUB/age-withdraw.
|
||||
*/
|
||||
struct TALER_AgeWithdrawCommitmentHashP h_commitment GNUNET_PACKED;
|
||||
|
||||
/**
|
||||
* Index that the client will not have to reveal, in NBO.
|
||||
* Must be smaller than #TALER_CNC_KAPPA.
|
||||
*/
|
||||
uint32_t noreveal_index GNUNET_PACKED;
|
||||
|
||||
};
|
||||
|
||||
GNUNET_NETWORK_STRUCT_END
|
||||
|
||||
enum TALER_ErrorCode
|
||||
TALER_exchange_online_age_withdraw_confirmation_sign (
|
||||
TALER_ExchangeSignCallback scb,
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
uint32_t noreveal_index,
|
||||
struct TALER_ExchangePublicKeyP *pub,
|
||||
struct TALER_ExchangeSignatureP *sig)
|
||||
{
|
||||
|
||||
struct TALER_AgeWithdrawConfirmationPS confirm = {
|
||||
.purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_AGE_WITHDRAW),
|
||||
.purpose.size = htonl (sizeof (confirm)),
|
||||
.h_commitment = *h_commitment,
|
||||
.noreveal_index = htonl (noreveal_index)
|
||||
};
|
||||
|
||||
return scb (&confirm.purpose,
|
||||
pub,
|
||||
sig);
|
||||
}
|
||||
|
||||
|
||||
/* TODO:oec: add signature for age-withdraw, age-reveal */
|
||||
|
||||
|
||||
GNUNET_NETWORK_STRUCT_BEGIN
|
||||
|
||||
/**
|
||||
|
@ -604,6 +604,92 @@ TALER_wallet_withdraw_verify (
|
||||
}
|
||||
|
||||
|
||||
GNUNET_NETWORK_STRUCT_BEGIN
|
||||
|
||||
/**
|
||||
* @brief Format used for to generate the signature on a request to
|
||||
* age-withdraw from a reserve.
|
||||
*/
|
||||
struct TALER_AgeWithdrawRequestPS
|
||||
{
|
||||
|
||||
/**
|
||||
* Purpose must be #TALER_SIGNATURE_WALLET_RESERVE_WITHDRAW.
|
||||
* Used with an EdDSA signature of a `struct TALER_ReservePublicKeyP`.
|
||||
*/
|
||||
struct GNUNET_CRYPTO_EccSignaturePurpose purpose;
|
||||
|
||||
/**
|
||||
* Hash of the commitment of n*kappa coins
|
||||
*/
|
||||
struct TALER_AgeWithdrawCommitmentHashP h_commitment GNUNET_PACKED;
|
||||
|
||||
/**
|
||||
* Value of the coin being exchanged (matching the denomination key)
|
||||
* plus the transaction fee. We include this in what is being
|
||||
* signed so that we can verify a reserve's remaining total balance
|
||||
* without needing to access the respective denomination key
|
||||
* information each time.
|
||||
*/
|
||||
struct TALER_AmountNBO amount_with_fee;
|
||||
|
||||
/**
|
||||
* Maximum age group that the coins are going to be restricted to.
|
||||
*/
|
||||
uint32_t max_age_group;
|
||||
};
|
||||
|
||||
|
||||
GNUNET_NETWORK_STRUCT_END
|
||||
|
||||
void
|
||||
TALER_wallet_age_withdraw_sign (
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
const struct TALER_Amount *amount_with_fee,
|
||||
uint32_t max_age_group,
|
||||
const struct TALER_ReservePrivateKeyP *reserve_priv,
|
||||
struct TALER_ReserveSignatureP *reserve_sig)
|
||||
{
|
||||
struct TALER_AgeWithdrawRequestPS req = {
|
||||
.purpose.size = htonl (sizeof (req)),
|
||||
.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW),
|
||||
.h_commitment = *h_commitment,
|
||||
.max_age_group = max_age_group
|
||||
};
|
||||
|
||||
TALER_amount_hton (&req.amount_with_fee,
|
||||
amount_with_fee);
|
||||
GNUNET_CRYPTO_eddsa_sign (&reserve_priv->eddsa_priv,
|
||||
&req,
|
||||
&reserve_sig->eddsa_signature);
|
||||
}
|
||||
|
||||
|
||||
enum GNUNET_GenericReturnValue
|
||||
TALER_wallet_age_withdraw_verify (
|
||||
const struct TALER_AgeWithdrawCommitmentHashP *h_commitment,
|
||||
const struct TALER_Amount *amount_with_fee,
|
||||
uint32_t max_age_group,
|
||||
const struct TALER_ReservePublicKeyP *reserve_pub,
|
||||
const struct TALER_ReserveSignatureP *reserve_sig)
|
||||
{
|
||||
struct TALER_AgeWithdrawRequestPS awsrd = {
|
||||
.purpose.size = htonl (sizeof (awsrd)),
|
||||
.purpose.purpose = htonl (TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW),
|
||||
.h_commitment = *h_commitment,
|
||||
.max_age_group = max_age_group
|
||||
};
|
||||
|
||||
TALER_amount_hton (&awsrd.amount_with_fee,
|
||||
amount_with_fee);
|
||||
return GNUNET_CRYPTO_eddsa_verify (
|
||||
TALER_SIGNATURE_WALLET_RESERVE_AGE_WITHDRAW,
|
||||
&awsrd,
|
||||
&reserve_sig->eddsa_signature,
|
||||
&reserve_pub->eddsa_pub);
|
||||
}
|
||||
|
||||
|
||||
GNUNET_NETWORK_STRUCT_BEGIN
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user