do_reserve_open.sql (9017B)
1 -- 2 -- This file is part of TALER 3 -- Copyright (C) 2014--2022 Taler Systems SA 4 -- 5 -- TALER is free software; you can redistribute it and/or modify it under the 6 -- terms of the GNU General Public License as published by the Free Software 7 -- Foundation; either version 3, or (at your option) any later version. 8 -- 9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 -- A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 -- 13 -- You should have received a copy of the GNU General Public License along with 14 -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 -- 16 17 CREATE OR REPLACE FUNCTION exchange_do_reserve_open( 18 IN in_reserve_pub BYTEA, 19 IN in_total_paid taler_amount, 20 IN in_reserve_payment taler_amount, 21 IN in_min_purse_limit INT4, 22 IN in_default_purse_limit INT4, 23 IN in_reserve_sig BYTEA, 24 IN in_desired_expiration INT8, 25 IN in_reserve_gc_delay INT8, 26 IN in_now INT8, 27 IN in_open_fee taler_amount, 28 OUT out_open_cost taler_amount, 29 OUT out_final_expiration INT8, 30 OUT out_no_reserve BOOLEAN, 31 OUT out_no_funds BOOLEAN, 32 OUT out_reserve_balance taler_amount) 33 LANGUAGE plpgsql 34 AS $$ 35 DECLARE 36 my_balance taler_amount; 37 my_cost taler_amount; 38 my_cost_tmp INT8; 39 my_years_tmp INT8; 40 my_years INT8; 41 my_purses_allowed INT8; 42 my_needs_update BOOL; 43 my_expiration_date INT8; 44 my_desired_expiration INT8; 45 reserve RECORD; 46 BEGIN 47 48 SELECT current_balance 49 ,expiration_date 50 ,purses_allowed 51 INTO reserve 52 FROM reserves 53 WHERE reserve_pub=in_reserve_pub; 54 55 IF NOT FOUND 56 THEN 57 RAISE NOTICE 'reserve not found'; 58 out_no_reserve = TRUE; 59 out_no_funds = TRUE; 60 out_reserve_balance.val = 0; 61 out_reserve_balance.frac = 0; 62 out_open_cost.val = 0; 63 out_open_cost.frac = 0; 64 out_final_expiration = 0; 65 RETURN; 66 END IF; 67 68 out_no_reserve = FALSE; 69 out_reserve_balance = reserve.current_balance; 70 71 -- Do not allow expiration time to start in the past already 72 IF (reserve.expiration_date < in_now) 73 THEN 74 my_expiration_date = in_now; 75 ELSE 76 my_expiration_date = reserve.expiration_date; 77 END IF; 78 79 my_cost.val = 0; 80 my_cost.frac = 0; 81 my_needs_update = FALSE; 82 my_years = 0; 83 84 -- Bound the requested expiration. GNUNET_JSON_spec_timestamp() accepts the 85 -- literal "never" and hands us INT64_MAX, and any t_s above roughly 9.2e12 86 -- seconds is clamped to the same value; the additions below would then 87 -- overflow INT8 (SQLSTATE 22003), which the caller can only turn into an 88 -- HTTP 500. Pricing such a request as if it had asked for the cap gives the 89 -- client the ordinary "here is what it would cost" answer -- which it cannot 90 -- pay, so it gets a 402 -- instead of an internal error. The cap is the end 91 -- of year 9999, the largest timestamp PostgreSQL itself can represent. 92 my_desired_expiration = LEAST (in_desired_expiration, 253402300799000000); 93 94 -- Compute years based on desired expiration time 95 IF (my_expiration_date < my_desired_expiration) 96 THEN 97 my_years = (31535999999999 + my_desired_expiration - my_expiration_date) / 31536000000000; 98 reserve.purses_allowed = in_default_purse_limit; 99 my_expiration_date = my_expiration_date + 31536000000000 * my_years; 100 END IF; 101 102 -- Increase years based on purses requested 103 IF (reserve.purses_allowed < in_min_purse_limit) 104 THEN 105 my_years = (31535999999999 + my_desired_expiration - in_now) / 31536000000000; 106 my_expiration_date = in_now + 31536000000000 * my_years; 107 -- in_min_purse_limit is an INT4 that comes straight from the client's 108 -- "purse_limit", so this sum has to be evaluated in INT8: in INT4 it 109 -- overflows before the division. 110 my_years_tmp = (in_min_purse_limit::INT8 + in_default_purse_limit - reserve.purses_allowed - 1) / in_default_purse_limit; 111 my_years = my_years + my_years_tmp; 112 my_purses_allowed = reserve.purses_allowed::INT8 + (in_default_purse_limit::INT8 * my_years_tmp); 113 ELSE 114 my_purses_allowed = reserve.purses_allowed; 115 END IF; 116 117 -- Refuse to price what we cannot represent, rather than overflowing further 118 -- down: reporting the maximum cost makes the request fail with "payment 119 -- required", where an arithmetic overflow can only become an HTTP 500. 120 IF ( (my_years > 8000) OR 121 (my_purses_allowed > 2147483647) ) 122 THEN 123 -- Just below TALER_AMOUNT_MAX_VALUE, the largest amount the C layer parses. 124 out_open_cost.val=4503599627370495; 125 out_open_cost.frac=99999999; 126 out_no_funds=FALSE; 127 -- We must return a failure, which the C layer detects by the returned 128 -- expiration being below the desired expiration. Returning 129 -- my_expiration_date here would be at or above the desired expiration 130 -- and thus be reported as a successful "reserve opened" (200 OK) even 131 -- though the UPDATE below never ran. 132 IF (reserve.expiration_date >= in_desired_expiration) 133 THEN 134 out_final_expiration = 0; 135 ELSE 136 out_final_expiration = reserve.expiration_date; 137 END IF; 138 RAISE NOTICE 'requested reserve lifetime or purse limit out of range'; 139 RETURN; 140 END IF; 141 142 -- Compute cost based on annual fees 143 IF (my_years > 0) 144 THEN 145 -- my_years and taler_amount.frac are multiplied in INT8: with my_years 146 -- declared INT4 this used to be an INT4 multiplication that overflowed 147 -- before the division, making the guard below unreachable. 148 my_cost.val = my_years * in_open_fee.val; 149 my_cost_tmp = my_years * in_open_fee.frac / 100000000; 150 IF (CAST (my_cost.val + my_cost_tmp AS INT8) < my_cost.val) 151 THEN 152 -- Must stay below TALER_AMOUNT_MAX_VALUE (and 100000000): the previous 153 -- sentinel (INT64_MAX, 2^31-1) is not a legal taler amount, so the C 154 -- layer refused to parse it and turned this branch into an HTTP 500 of 155 -- its own. 156 out_open_cost.val=4503599627370495; 157 out_open_cost.frac=99999999; 158 out_no_funds=FALSE; 159 -- As above: the failure must be signalled by an expiration below the 160 -- desired expiration, or the C layer reports 200 OK for a reserve 161 -- that was never opened. 162 IF (reserve.expiration_date >= in_desired_expiration) 163 THEN 164 out_final_expiration = 0; 165 ELSE 166 out_final_expiration = reserve.expiration_date; 167 END IF; 168 RAISE NOTICE 'arithmetic issue computing amount'; 169 RETURN; 170 END IF; 171 my_cost.val = CAST (my_cost.val + my_cost_tmp AS INT8); 172 my_cost.frac = my_years * in_open_fee.frac % 100000000; 173 my_needs_update = TRUE; 174 END IF; 175 176 -- check if we actually have something to do 177 IF NOT my_needs_update 178 THEN 179 out_final_expiration = reserve.expiration_date; 180 out_open_cost.val = 0; 181 out_open_cost.frac = 0; 182 out_no_funds=FALSE; 183 RAISE NOTICE 'no change required'; 184 RETURN; 185 END IF; 186 187 -- Check payment (coins and reserve) would be sufficient. 188 IF ( (in_total_paid.val < my_cost.val) OR 189 ( (in_total_paid.val = my_cost.val) AND 190 (in_total_paid.frac < my_cost.frac) ) ) 191 THEN 192 out_open_cost.val = my_cost.val; 193 out_open_cost.frac = my_cost.frac; 194 out_no_funds=FALSE; 195 -- We must return a failure, which is indicated by 196 -- the expiration being below the desired expiration. 197 IF (reserve.expiration_date >= in_desired_expiration) 198 THEN 199 -- This case is relevant especially if the purse 200 -- count was to be increased and the payment was 201 -- insufficient to cover this for the full period. 202 RAISE NOTICE 'forcing low expiration time'; 203 out_final_expiration = 0; 204 ELSE 205 out_final_expiration = reserve.expiration_date; 206 END IF; 207 RAISE NOTICE 'amount paid too low'; 208 RETURN; 209 END IF; 210 211 -- Check reserve balance is sufficient. 212 IF (out_reserve_balance.val > in_reserve_payment.val) 213 THEN 214 IF (out_reserve_balance.frac >= in_reserve_payment.frac) 215 THEN 216 my_balance.val=out_reserve_balance.val - in_reserve_payment.val; 217 my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac; 218 ELSE 219 my_balance.val=out_reserve_balance.val - in_reserve_payment.val - 1; 220 my_balance.frac=out_reserve_balance.frac + 100000000 - in_reserve_payment.frac; 221 END IF; 222 ELSE 223 IF (out_reserve_balance.val = in_reserve_payment.val) AND (out_reserve_balance.frac >= in_reserve_payment.frac) 224 THEN 225 my_balance.val=0; 226 my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac; 227 ELSE 228 out_final_expiration = reserve.expiration_date; 229 out_open_cost.val = my_cost.val; 230 out_open_cost.frac = my_cost.frac; 231 out_no_funds=TRUE; 232 RAISE NOTICE 'reserve balance too low'; 233 RETURN; 234 END IF; 235 END IF; 236 237 UPDATE reserves SET 238 current_balance=my_balance 239 ,gc_date=reserve.expiration_date + in_reserve_gc_delay 240 ,expiration_date=my_expiration_date 241 ,purses_allowed=my_purses_allowed 242 WHERE 243 reserve_pub=in_reserve_pub; 244 245 INSERT INTO reserves_open_requests 246 (reserve_pub 247 ,request_timestamp 248 ,expiration_date 249 ,reserve_sig 250 ,reserve_payment 251 ,requested_purse_limit 252 ) VALUES ( 253 in_reserve_pub 254 ,in_now 255 ,in_desired_expiration 256 ,in_reserve_sig 257 ,in_reserve_payment 258 ,in_min_purse_limit) 259 ON CONFLICT DO NOTHING; 260 261 out_final_expiration=my_expiration_date; 262 out_open_cost = my_cost; 263 out_no_funds=FALSE; 264 RETURN; 265 266 END $$;