mte/include/sql/exchange_do_withdraw.sql
2025-11-11 10:03:07 +01:00

229 lines
6.2 KiB
PL/PgSQL

--
-- This file is part of TALER
-- Copyright (C) 2023-2025 Taler Systems SA
--
-- TALER is free software; you can redistribute it and/or modify it under the
-- terms of the GNU General Public License as published by the Free Software
-- Foundation; either version 3, or (at your option) any later version.
--
-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License along with
-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
DROP FUNCTION IF EXISTS exchange_do_withdraw;
CREATE FUNCTION exchange_do_withdraw(
IN in_amount_with_fee taler_amount,
IN in_reserve_pub BYTEA,
IN in_reserve_sig BYTEA,
IN in_now INT8,
IN in_min_reserve_gc INT8,
IN in_planchets_h BYTEA,
IN in_maximum_age_committed INT2, -- in years ϵ [0,1..), possibly NULL
IN in_noreveal_index INT2, -- possibly NULL (if not age-withdraw)
IN in_selected_h BYTEA, -- possibly NULL (if not age-withdraw)
IN in_denom_serials INT8[],
IN in_denom_sigs BYTEA[],
IN in_blinding_seed BYTEA, -- possibly NULL (if no CS denominations)
IN in_cs_r_values BYTEA[], -- possibly NULL (if no CS denominations)
IN in_cs_r_choices INT8, -- possibly NULL (if no CS denominations)
OUT out_reserve_found BOOLEAN,
OUT out_balance_ok BOOLEAN,
OUT out_reserve_balance taler_amount,
OUT out_age_ok BOOLEAN,
OUT out_required_age INT2, -- in years ϵ [0,1..)
OUT out_reserve_birthday INT4,
OUT out_idempotent BOOLEAN,
OUT out_noreveal_index INT2, -- possibly NULL (if not age-withdraw)
OUT out_nonce_reuse BOOLEAN)
LANGUAGE plpgsql
AS $$
DECLARE
my_reserve RECORD;
my_difference RECORD;
my_balance taler_amount;
my_not_before DATE;
my_earliest_date DATE;
BEGIN
-- Shards: reserves by reserve_pub (SELECT)
-- reserves by reserve_pub (UPDATE)
-- First, find the reserve
SELECT current_balance
,birthday
,gc_date
INTO my_reserve
FROM reserves
WHERE reserve_pub=in_reserve_pub;
out_reserve_found = FOUND;
IF NOT out_reserve_found
THEN
out_age_ok = FALSE;
out_required_age = -1;
out_idempotent = FALSE;
out_noreveal_index = -1;
out_reserve_balance.val = 0;
out_reserve_balance.frac = 0;
out_balance_ok = FALSE;
out_nonce_reuse = FALSE;
out_reserve_birthday = 0;
RETURN;
END IF;
out_reserve_balance = my_reserve.current_balance;
out_reserve_birthday = my_reserve.birthday;
-- FIXME-performance: probably better to INSERT and on-conflict check for idempotency...
-- Next, check for idempotency of the withdraw
SELECT noreveal_index
INTO out_noreveal_index
FROM withdraw
WHERE reserve_pub = in_reserve_pub
AND planchets_h = in_planchets_h;
out_idempotent = FOUND;
IF out_idempotent
THEN
-- out_idempotent set, out_noreveal_index possibly set, report.
out_balance_ok = TRUE;
out_age_ok = TRUE;
out_required_age = -1;
out_nonce_reuse = FALSE;
RETURN;
END IF;
out_noreveal_index = -1;
-- Check age requirements
IF (my_reserve.birthday <> 0)
THEN
my_not_before=date '1970-01-01' + my_reserve.birthday;
my_earliest_date = current_date - make_interval(in_maximum_age_committed);
--
-- 1970-01-01 + birthday == my_not_before now
-- | | |
-- <.......not allowed......>[<.....allowed range......>]
-- | | |
-- ____*_____________________*_________*________________* timeline
-- |
-- my_earliest_date ==
-- now - maximum_age_committed*year
--
IF ( (in_maximum_age_committed IS NULL) OR
(my_earliest_date < my_not_before) )
THEN
out_required_age = extract(year FROM age(current_date, my_not_before));
out_age_ok = FALSE;
out_balance_ok = TRUE; -- not really
out_nonce_reuse = FALSE; -- not really
RETURN;
END IF;
END IF;
out_age_ok = TRUE;
out_required_age = 0;
-- Check reserve balance is sufficient.
SELECT *
INTO my_difference
FROM amount_left_minus_right(out_reserve_balance
,in_amount_with_fee);
out_balance_ok = my_difference.ok;
IF NOT out_balance_ok
THEN
out_nonce_reuse = FALSE; -- not yet determined
RETURN;
END IF;
my_balance = my_difference.diff;
-- Calculate new expiration dates.
in_min_reserve_gc=GREATEST(in_min_reserve_gc,my_reserve.gc_date);
-- Update reserve balance.
UPDATE reserves SET
gc_date=in_min_reserve_gc
,current_balance=my_balance
WHERE
reserve_pub=in_reserve_pub;
-- Ensure the uniqueness of the blinding_seed
IF in_blinding_seed IS NOT NULL
THEN
INSERT INTO unique_withdraw_blinding_seed
(blinding_seed)
VALUES
(in_blinding_seed)
ON CONFLICT DO NOTHING;
IF NOT FOUND
THEN
out_nonce_reuse = TRUE;
RETURN;
END IF;
END IF;
out_nonce_reuse = FALSE;
-- Write the data into the withdraw table
INSERT INTO withdraw
(planchets_h
,execution_date
,max_age
,amount_with_fee
,reserve_pub
,reserve_sig
,noreveal_index
,denom_serials
,selected_h
,blinding_seed
,cs_r_values
,cs_r_choices
,denom_sigs)
VALUES
(in_planchets_h
,in_now
,in_maximum_age_committed
,in_amount_with_fee
,in_reserve_pub
,in_reserve_sig
,in_noreveal_index
,in_denom_serials
,in_selected_h
,in_blinding_seed
,in_cs_r_values
,in_cs_r_choices
,in_denom_sigs)
ON CONFLICT DO NOTHING;
IF NOT FOUND
THEN
RAISE EXCEPTION 'Conflict on insert into withdraw despite idempotency check for reserve_pub(%) and planchets_h(%)!',
in_reserve_pub,
in_planchets_h;
END IF;
END $$;
COMMENT ON FUNCTION exchange_do_withdraw(
taler_amount,
BYTEA,
BYTEA,
INT8,
INT8,
BYTEA,
INT2,
INT2,
BYTEA,
INT8[],
BYTEA[],
BYTEA,
BYTEA[],
INT8)
IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the hashes of all blinded planchets, (separately) the hashes of the chosen planchets and denomination signatures, or signaling idempotency (and previous noreveal_index) or nonce reuse';