introduce stored procedure for coin balance check

This commit is contained in:
Christian Grothoff 2021-12-09 23:13:39 +01:00
parent 889625a90f
commit fba91c63d5
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
5 changed files with 367 additions and 18 deletions

View File

@ -164,8 +164,23 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin,
} }
enum GNUNET_DB_QueryStatus /**
TEH_check_coin_balance (struct MHD_Connection *connection, * Called when we actually know that the balance (was) insufficient.
* Re-does the check (slowly) to compute the full error message for
* the client.
*
* @param connection HTTP connection to report hard errors on
* @param coin_pub coin to analyze
* @param coin_value total value of the original coin (by denomination)
* @param op_cost cost of the current operation (for error reporting)
* @param check_recoup should we include recoup transactions in the check
* @param zombie_required additional requirement that the coin must
* be a zombie coin, or also hard failure
* @param[out] mhd_ret set to response status code, on hard error only
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
check_coin_balance (struct MHD_Connection *connection,
const struct TALER_CoinSpendPublicKeyP *coin_pub, const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *coin_value, const struct TALER_Amount *coin_value,
const struct TALER_Amount *op_cost, const struct TALER_Amount *op_cost,
@ -273,13 +288,90 @@ TEH_check_coin_balance (struct MHD_Connection *connection,
return GNUNET_DB_STATUS_HARD_ERROR; return GNUNET_DB_STATUS_HARD_ERROR;
} }
/* we're good, coin has sufficient funds to be melted */ /* This should not happen: The coin has sufficient funds
after all!?!? */
TEH_plugin->free_coin_transaction_list (TEH_plugin->cls, TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
tl); tl);
return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
} }
enum GNUNET_DB_QueryStatus
TEH_check_coin_balance (struct MHD_Connection *connection,
const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *coin_value,
const struct TALER_Amount *op_cost,
bool check_recoup,
bool zombie_required,
MHD_RESULT *mhd_ret)
{
bool balance_ok = false;
bool zombie_ok = false;
enum GNUNET_DB_QueryStatus qs;
qs = TEH_plugin->do_check_coin_balance (TEH_plugin->cls,
coin_pub,
coin_value,
check_recoup,
zombie_required,
&balance_ok,
&zombie_ok);
switch (qs)
{
case GNUNET_DB_STATUS_HARD_ERROR:
*mhd_ret = TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"check_coin_balance");
return qs;
case GNUNET_DB_STATUS_SOFT_ERROR:
return qs;
case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
GNUNET_break (0);
*mhd_ret = TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"check_coin_balance");
return GNUNET_DB_STATUS_HARD_ERROR;
case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
/* handled below */
break;
}
if (! zombie_ok)
{
GNUNET_break_op (0);
*mhd_ret = TALER_MHD_reply_with_error (
connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE,
NULL);
return GNUNET_DB_STATUS_HARD_ERROR;
}
if (balance_ok)
return qs;
/* balance is not OK, do expensive call to compute full error message */
qs = check_coin_balance (connection,
coin_pub,
coin_value,
op_cost,
check_recoup,
zombie_required,
mhd_ret);
if (qs < 0)
return qs; /* we expected to fail (same check as before!) */
GNUNET_break (0); /* stored procedure and individual statements
disagree, should be impossible! */
*mhd_ret = TALER_MHD_reply_with_error (
connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_INVARIANT_FAILURE,
"stored procedure disagrees with full coin transaction history fetch");
return GNUNET_DB_STATUS_HARD_ERROR;
}
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TEH_DB_run_transaction (struct MHD_Connection *connection, TEH_DB_run_transaction (struct MHD_Connection *connection,
const char *name, const char *name,

View File

@ -47,6 +47,9 @@ TEH_make_coin_known (const struct TALER_CoinPublicInfo *coin,
* insufficient for all transactions associated with the * insufficient for all transactions associated with the
* coin, return a hard error. * coin, return a hard error.
* *
* We first do a "fast" check using a stored procedure, and
* only obtain the "full" data on failure (for performance).
*
* @param connection HTTP connection to report hard errors on * @param connection HTTP connection to report hard errors on
* @param coin_pub coin to analyze * @param coin_pub coin to analyze
* @param coin_value total value of the original coin (by denomination) * @param coin_value total value of the original coin (by denomination)

View File

@ -899,6 +899,194 @@ COMMENT ON FUNCTION exchange_do_withdraw_limit_check(INT8, INT8, INT8, INT4)
CREATE OR REPLACE FUNCTION exchange_do_check_coin_balance(
IN denom_val INT8, -- value of the denomination of the coin
IN denom_frac INT4, -- value of the denomination of the coin
IN in_coin_pub BYTEA, -- coin public key
IN check_recoup BOOLEAN, -- do we need to check the recoup table?
IN zombie_required BOOLEAN, -- do we need a zombie coin?
OUT balance_ok BOOLEAN, -- balance satisfied?
OUT zombie_ok BOOLEAN) -- zombie satisfied?
LANGUAGE plpgsql
AS $$
DECLARE
coin_uuid INT8; -- known_coin_id of coin_pub
DECLARE
tmp_val INT8; -- temporary result
DECLARE
tmp_frac INT8; -- temporary result
DECLARE
spent_val INT8; -- how much of coin was spent?
DECLARE
spent_frac INT8; -- how much of coin was spent?
DECLARE
unspent_val INT8; -- how much of coin was refunded?
DECLARE
unspent_frac INT8; -- how much of coin was refunded?
BEGIN
-- Note: possible future optimization: get the coin_uuid from the previous
-- 'ensure_coin_known' and pass that here instead of the coin_pub. Might help
-- a tiny bit with performance.
SELECT known_coin_id INTO coin_uuid
FROM known_coins
WHERE coin_pub=in_coin_pub;
IF NOT FOUND
THEN
-- coin unknown, should be impossible!
balance_ok=FALSE;
zombie_ok=FALSE;
ASSERT false, 'coin unknown';
RETURN;
END IF;
spent_val = 0;
spent_frac = 0;
unspent_val = denom_val;
unspent_frac = denom_frac;
SELECT
SUM(amount_with_fee_val) -- overflow here is not plausible
,SUM(CAST(amount_with_fee_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM deposits
WHERE known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
spent_val = spent_val + tmp_val;
spent_frac = spent_frac + tmp_frac;
END IF;
SELECT
SUM(amount_with_fee_val) -- overflow here is not plausible
,SUM(CAST(amount_with_fee_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM refresh_commitments
WHERE old_known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
spent_val = spent_val + tmp_val;
spent_frac = spent_frac + tmp_frac;
END IF;
SELECT
SUM(rf.amount_with_fee_val) -- overflow here is not plausible
,SUM(CAST(rf.amount_with_fee_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM deposits
JOIN refunds rf
USING (deposit_serial_id)
WHERE
known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
unspent_val = unspent_val + tmp_val;
unspent_frac = unspent_frac + tmp_frac;
END IF;
-- Note: even if 'check_recoup' is true, the tables below
-- are in practice likely empty (as they only apply if
-- the exchange (ever) had to revoke keys).
IF check_recoup
THEN
SELECT
SUM(amount_val) -- overflow here is not plausible
,SUM(CAST(amount_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM recoup_refresh
WHERE known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
spent_val = spent_val + tmp_val;
spent_frac = spent_frac + tmp_frac;
END IF;
SELECT
SUM(amount_val) -- overflow here is not plausible
,SUM(CAST(amount_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM recoup
WHERE known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
spent_val = spent_val + tmp_val;
spent_frac = spent_frac + tmp_frac;
END IF;
SELECT
SUM(amount_val) -- overflow here is not plausible
,SUM(CAST(amount_frac AS INT8)) -- compute using 64 bits
INTO
tmp_val
,tmp_frac
FROM recoup_refresh
JOIN refresh_revealed_coins rrc
USING (rrc_serial)
JOIN refresh_commitments rfc
ON (rrc.melt_serial_id = rfc.melt_serial_id)
WHERE rfc.old_known_coin_id=coin_uuid;
IF tmp_val IS NOT NULL
THEN
unspent_val = unspent_val + tmp_val;
unspent_frac = unspent_frac + tmp_frac;
END IF;
IF ( (0 < tmp_val) OR (0 < tmp_frac) )
THEN
-- There was a transaction that justifies the zombie
-- status, clear the flag
zombie_required=FALSE;
END IF;
END IF;
-- normalize results
spent_val = spent_val + spent_frac / 100000000;
spent_frac = spent_frac % 100000000;
unspent_val = unspent_val + unspent_frac / 100000000;
unspent_frac = unspent_frac % 100000000;
-- Actually check if the coin balance is sufficient. Verbosely. ;-)
IF (unspent_val > spent_val)
THEN
balance_ok=TRUE;
ELSE
IF (unspent_val = spent_val) AND (unspent_frac >= spent_frac)
THEN
balance_ok=TRUE;
ELSE
balance_ok=FALSE;
END IF;
END IF;
zombie_ok = NOT zombie_required;
END $$;
COMMENT ON FUNCTION exchange_do_check_coin_balance(INT8, INT4, BYTEA, BOOLEAN, BOOLEAN)
IS 'Checks whether the coin has sufficient balance for all the operations associated with it';
-- Complete transaction -- Complete transaction

View File

@ -596,6 +596,16 @@ prepare_statements (struct PostgresClosure *pg)
"lock_withdraw", "lock_withdraw",
"LOCK TABLE reserves_out;", "LOCK TABLE reserves_out;",
0), 0),
/* Used in #postgres_do_check_coin_balance() to check
a coin's balance */
GNUNET_PQ_make_prepare (
"call_check_coin_balance",
"SELECT "
" balance_ok"
",zombie_ok"
" FROM exchange_do_check_coin_balance"
" ($1,$2,$3,$4,$5);",
5),
/* Used in #postgres_do_withdraw() to store /* Used in #postgres_do_withdraw() to store
the signature of a blinded coin with the blinded coin's the signature of a blinded coin with the blinded coin's
details before returning it during /reserve/withdraw. We store details before returning it during /reserve/withdraw. We store
@ -4491,6 +4501,53 @@ postgres_get_withdraw_info (
} }
/**
* Check coin balance is sufficient to satisfy balance
* invariants.
*
* @param cls the `struct PostgresClosure` with the plugin-specific state
* @param coin_pub coin to check
* @param coin_value value of the coin's denomination (avoids internal lookup)
* @param check_recoup include recoup and recoup_refresh tables in calculation
* @param zombie_required additionally require coin to be a zombie coin
* @param[out] balance_ok set to true if the balance was sufficient
* @param[out] zombie_ok set to true if the zombie requirement was satisfied
* @return query execution status
*/
static enum GNUNET_DB_QueryStatus
postgres_do_check_coin_balance (
void *cls,
const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *coin_value,
bool check_recoup,
bool zombie_required,
bool *balance_ok,
bool *zombie_ok)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
TALER_PQ_query_param_amount (coin_value),
GNUNET_PQ_query_param_auto_from_type (coin_pub),
GNUNET_PQ_query_param_bool (check_recoup),
GNUNET_PQ_query_param_bool (zombie_required),
GNUNET_PQ_query_param_end
};
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_bool ("balance_ok",
balance_ok),
GNUNET_PQ_result_spec_bool ("zombie_ok",
zombie_ok),
GNUNET_PQ_result_spec_end
};
return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"call_check_coin_balance",
params,
rs);
}
/** /**
* Perform withdraw operation, checking for sufficient balance * Perform withdraw operation, checking for sufficient balance
* and possibly persisting the withdrawal details. * and possibly persisting the withdrawal details.
@ -11825,7 +11882,7 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
plugin->get_latest_reserve_in_reference = plugin->get_latest_reserve_in_reference =
&postgres_get_latest_reserve_in_reference; &postgres_get_latest_reserve_in_reference;
plugin->get_withdraw_info = &postgres_get_withdraw_info; plugin->get_withdraw_info = &postgres_get_withdraw_info;
// plugin->insert_withdraw_info = &postgres_insert_withdraw_info; plugin->do_check_coin_balance = &postgres_do_check_coin_balance;
plugin->do_withdraw = &postgres_do_withdraw; plugin->do_withdraw = &postgres_do_withdraw;
plugin->do_withdraw_limit_check = &postgres_do_withdraw_limit_check; plugin->do_withdraw_limit_check = &postgres_do_withdraw_limit_check;
plugin->get_reserve_history = &postgres_get_reserve_history; plugin->get_reserve_history = &postgres_get_reserve_history;

View File

@ -2503,18 +2503,27 @@ struct TALER_EXCHANGEDB_Plugin
/** /**
* Store collectable coin under the corresponding hash of the blinded * Check coin balance is sufficient to satisfy balance
* message. * invariants.
* *
* @param cls the @e cls of this struct with the plugin-specific state * @param cls the `struct PostgresClosure` with the plugin-specific state
* @param collectable corresponding collectable coin (blind signature) * @param coin_pub coin to check
* if a coin is found * @param coin_value value of the coin's denomination (avoids internal lookup)
* @return statement execution status * @param check_recoup include recoup and recoup_refresh tables in calculation
* @param zombie_required additionally require coin to be a zombie coin
* @param[out] balance_ok set to true if the balance was sufficient
* @param[out] zombie_ok set to true if the zombie requirement was satisfied
* @return query execution status
*/ */
enum GNUNET_DB_QueryStatus enum GNUNET_DB_QueryStatus
(*insert_withdraw_infoXX)( (*do_check_coin_balance)(
void *cls, void *cls,
const struct TALER_EXCHANGEDB_CollectableBlindcoin *collectable); const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *coin_value,
bool check_recoup,
bool zombie_required,
bool *balance_ok,
bool *zombie_ok);
/** /**