diff --git a/src/exchangedb/0003-age_withdraw.sql b/src/exchangedb/0003-age_withdraw.sql index 9816e466c..c85eb60f5 100644 --- a/src/exchangedb/0003-age_withdraw.sql +++ b/src/exchangedb/0003-age_withdraw.sql @@ -29,8 +29,7 @@ BEGIN '(age_withdraw_id BIGINT GENERATED BY DEFAULT AS IDENTITY' ',h_commitment BYTEA NOT NULL CONSTRAINT h_commitment_length CHECK(LENGTH(h_commitment)=64)' ',max_age SMALLINT NOT NULL CONSTRAINT max_age_positive CHECK(max_age>=0)' - ',amount_with_fee_val INT8 NOT NULL' - ',amount_with_fee_frac INT4 NOT NULL' + ',amount_with_fee TALER_AMOUNT NOT NULL' ',reserve_pub BYTEA NOT NULL CONSTRAINT reserve_pub_length CHECK(LENGTH(reserve_pub)=32)' ',reserve_sig BYTEA NOT NULL CONSTRAINT reserve_sig_length CHECK(LENGTH(reserve_sig)=64)' ',noreveal_index SMALLINT NOT NULL CONSTRAINT noreveal_index_positive CHECK(noreveal_index>=0)' diff --git a/src/exchangedb/exchange-0001.sql b/src/exchangedb/exchange-0001.sql index d08aab4ea..79a0dec13 100644 --- a/src/exchangedb/exchange-0001.sql +++ b/src/exchangedb/exchange-0001.sql @@ -137,6 +137,97 @@ COMMENT ON FUNCTION comment_partitioned_column IS 'Generic function to create a comment on column of a table that is partitioned.'; +-------------------------------------------------------------- +-- Taler amounts and helper functiosn +------------------------------------------------------------- + +DO $$ +BEGIN + CREATE TYPE TALER_AMOUNT + AS (val INT8 + ,frac INT4); + + COMMENT ON TYPE TALER_AMOUNT + IS 'Type to store a TALER-amount as (val, frac) pair.'; +EXCEPTION + WHEN duplicate_object THEN null; +END +$$; + +CREATE PROCEDURE amount_normalize( + IN amount TALER_AMOUNT + ,OUT normalized TALER_AMOUNT +) +LANGUAGE plpgsql +AS $$ +BEGIN + normalized.val = amount.val + amount.frac / 100000000; + normalized.frac = amount.frac % 100000000; +END $$; + +COMMENT ON PROCEDURE amount_normalize + IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.'; + +CREATE PROCEDURE amount_add( + IN a TALER_AMOUNT + ,IN b TALER_AMOUNT + ,OUT sum TALER_AMOUNT +) +LANGUAGE plpgsql +AS $$ +BEGIN + sum = (a.val + b.val, a.frac + b.frac); + CALL amount_normalize(sum ,sum); + + IF (sum.val > (1<<52)) + THEN + RAISE EXCEPTION 'addition overflow'; + END IF; +END $$; + +COMMENT ON PROCEDURE amount_add + IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52'; + +CREATE FUNCTION amount_left_minus_right( + IN l TALER_AMOUNT + ,IN r TALER_AMOUNT + ,OUT diff TALER_AMOUNT + ,OUT ok BOOLEAN +) +LANGUAGE plpgsql +AS $$ +BEGIN + +IF (l.val > r.val) +THEN + ok = TRUE; + IF (l.frac >= r.frac) + THEN + diff.val = l.val - r.val; + diff.frac = l.frac - r.frac; + ELSE + diff.val = l.val - r.val - 1; + diff.frac = l.frac + 100000000 - r.frac; + END IF; +ELSE + IF (l.val = r.val) AND (l.frac >= r.frac) + THEN + diff.val = 0; + diff.frac = l.frac - r.frac; + ok = TRUE; + ELSE + diff = (-1, -1); + ok = FALSE; + END IF; +END IF; + +RETURN; +END $$; + +COMMENT ON FUNCTION amount_left_minus_right + IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.'; + + --------------------------------------------------------------------------- -- Main DB setup loop --------------------------------------------------------------------------- diff --git a/src/exchangedb/exchange_do_age_withdraw.sql b/src/exchangedb/exchange_do_age_withdraw.sql index 49a1433fd..e76d4ba75 100644 --- a/src/exchangedb/exchange_do_age_withdraw.sql +++ b/src/exchangedb/exchange_do_age_withdraw.sql @@ -16,8 +16,7 @@ -- @author Özgür Kesim CREATE OR REPLACE FUNCTION exchange_do_age_withdraw( - IN amount_val INT8, - IN amount_frac INT4, + IN amount_with_fee TALER_AMOUNT, IN rpub BYTEA, IN rsig BYTEA, IN now INT8, @@ -38,8 +37,9 @@ LANGUAGE plpgsql AS $$ DECLARE reserve_gc INT8; - reserve_val INT8; - reserve_frac INT4; + difference RECORD; + balance TALER_AMOUNT; + new_balance TALER_AMOUNT; not_before date; earliest_date date; BEGIN @@ -55,8 +55,8 @@ SELECT ,gc_date ,birthday INTO - reserve_val - ,reserve_frac + balance.val + ,balance.frac ,reserve_gc ,reserve_birthday FROM exchange.reserves @@ -102,31 +102,23 @@ END IF; age_ok = TRUE; required_age=0; - - -- Check reserve balance is sufficient. -IF (reserve_val > amount_val) -THEN - IF (reserve_frac >= amount_frac) - THEN - reserve_val=reserve_val - amount_val; - reserve_frac=reserve_frac - amount_frac; - ELSE - reserve_val=reserve_val - amount_val - 1; - reserve_frac=reserve_frac + 100000000 - amount_frac; - END IF; -ELSE - IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac) - THEN - reserve_val=0; - reserve_frac=reserve_frac - amount_frac; - ELSE - balance_ok=FALSE; - RETURN; - END IF; +SELECT * +INTO + difference +FROM + amount_left_minus_right( + balance + ,amount_with_fee); + +balance_ok = difference.ok; + +IF NOT balance_ok +THEN + RETURN; END IF; -balance_ok=TRUE; +new_balance = difference.diff; -- Calculate new expiration dates. min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); @@ -134,8 +126,8 @@ min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); -- Update reserve balance. UPDATE reserves SET gc_date=min_reserve_gc - ,current_balance_val=reserve_val - ,current_balance_frac=reserve_frac + ,current_balance_val=new_balance.val + ,current_balance_frac=new_balance.frac WHERE reserves.reserve_pub=rpub; @@ -143,8 +135,7 @@ WHERE INSERT INTO exchange.age_withdraw (h_commitment ,max_age - ,amount_with_fee_val - ,amount_with_fee_frac + ,amount_with_fee ,reserve_pub ,reserve_sig ,noreveal_index @@ -154,8 +145,7 @@ INSERT INTO exchange.age_withdraw VALUES (h_commitment ,maximum_age_committed - ,amount_val - ,amount_frac + ,amount_with_fee ,rpub ,rsig ,noreveal_index @@ -176,6 +166,6 @@ END IF; END $$; -COMMENT ON FUNCTION exchange_do_age_withdraw(INT8, INT4, BYTEA, BYTEA, INT8, INT8, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[]) +COMMENT ON FUNCTION exchange_do_age_withdraw(TALER_AMOUNT, BYTEA, BYTEA, INT8, INT8, BYTEA, INT2, INT2, BYTEA[], INT8[], BYTEA[]) IS 'Checks whether the reserve has sufficient balance for an age-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 blinded planchets and denomination signatures, or signaling conflict'; diff --git a/src/exchangedb/pg_do_age_withdraw.c b/src/exchangedb/pg_do_age_withdraw.c index 4fb52d466..0c79f58c9 100644 --- a/src/exchangedb/pg_do_age_withdraw.c +++ b/src/exchangedb/pg_do_age_withdraw.c @@ -44,7 +44,8 @@ TEH_PG_do_age_withdraw ( struct PostgresClosure *pg = cls; struct GNUNET_TIME_Timestamp gc; struct GNUNET_PQ_QueryParam params[] = { - TALER_PQ_query_param_amount (&commitment->amount_with_fee), + TALER_PQ_query_param_amount_tuple (pg->conn, + &commitment->amount_with_fee), GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_pub), GNUNET_PQ_query_param_auto_from_type (&commitment->reserve_sig), GNUNET_PQ_query_param_timestamp (&now), @@ -96,7 +97,7 @@ TEH_PG_do_age_withdraw ( ",reserve_birthday" ",conflict" " FROM exchange_do_age_withdraw" - " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12);"); + " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);"); return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "call_age_withdraw", params, diff --git a/src/exchangedb/pg_get_age_withdraw.c b/src/exchangedb/pg_get_age_withdraw.c index a66051c71..62ccaa834 100644 --- a/src/exchangedb/pg_get_age_withdraw.c +++ b/src/exchangedb/pg_get_age_withdraw.c @@ -48,8 +48,9 @@ TEH_PG_get_age_withdraw ( &aw->reserve_pub), GNUNET_PQ_result_spec_uint16 ("max_age", &aw->max_age), - TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee", - &aw->amount_with_fee), + TALER_PQ_result_spec_amount_tuple ("amount_with_fee", + pg->currency, + &aw->amount_with_fee), GNUNET_PQ_result_spec_uint16 ("noreveal_index", &aw->noreveal_index), TALER_PQ_result_spec_array_blinded_coin_hash ( @@ -83,8 +84,7 @@ TEH_PG_get_age_withdraw ( ",reserve_sig" ",reserve_pub" ",max_age" - ",amount_with_fee_val" - ",amount_with_fee_frac" + ",amount_with_fee" ",noreveal_index" ",h_blind_evs" ",denom_sigs" diff --git a/src/exchangedb/test_exchangedb.c b/src/exchangedb/test_exchangedb.c index ac3735b2d..36f51120a 100644 --- a/src/exchangedb/test_exchangedb.c +++ b/src/exchangedb/test_exchangedb.c @@ -1421,6 +1421,8 @@ run (void *cls) bool found; bool nonce_ok; bool balance_ok; + bool age_ok; + uint16_t maximum_age; uint64_t ruuid; FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != @@ -1428,9 +1430,12 @@ run (void *cls) NULL, &cbc, now, + false, &found, &balance_ok, &nonce_ok, + &age_ok, + &maximum_age, &ruuid)); GNUNET_assert (found); GNUNET_assert (nonce_ok); diff --git a/src/include/taler_pq_lib.h b/src/include/taler_pq_lib.h index db77c53b0..43561703b 100644 --- a/src/include/taler_pq_lib.h +++ b/src/include/taler_pq_lib.h @@ -51,6 +51,19 @@ TALER_PQ_query_param_amount_nbo (const struct TALER_AmountNBO *x); struct GNUNET_PQ_QueryParam TALER_PQ_query_param_amount (const struct TALER_Amount *x); +/** + * Generate query parameter (as record tuple) for an amount, consisting + * of the two components "value" and "fraction" in this order. The + * types must be a 64-bit integer and a 32-bit integer + * respectively. The currency is dropped. + * + * @param db The database context for OID lookup + * @param amount pointer to the query parameter to pass + */ +struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_amount_tuple ( + const struct GNUNET_PQ_Context *db, + const struct TALER_Amount *amount); /** * Generate query parameter for a denomination public @@ -180,6 +193,18 @@ TALER_PQ_result_spec_amount (const char *name, const char *currency, struct TALER_Amount *amount); +/** + * Currency amount expected, from a record-field of (DB) taler_amount type + * + * @param name name of the field in the table + * @param currency currency to use for @a amount + * @param[out] amount where to store the result + * @return array entry for the result specification to use + */ +struct GNUNET_PQ_ResultSpec +TALER_PQ_result_spec_amount_tuple (const char *name, + const char *currency, + struct TALER_Amount *amount); /** * Denomination public key expected. diff --git a/src/pq/pq_query_helper.c b/src/pq/pq_query_helper.c index 9a02cddab..9ada23575 100644 --- a/src/pq/pq_query_helper.c +++ b/src/pq/pq_query_helper.c @@ -21,6 +21,7 @@ * @author Christian Grothoff */ #include "platform.h" +#include #include #include #include "taler_pq_lib.h" @@ -149,6 +150,84 @@ TALER_PQ_query_param_amount (const struct TALER_Amount *x) } +/** + * Function called to convert input amount into SQL parameter as tuple. + * + * @param cls closure + * @param data pointer to input argument, here a `struct TALER_Amount` + * @param data_len number of bytes in @a data (if applicable) + * @param[out] param_values SQL data to set + * @param[out] param_lengths SQL length data to set + * @param[out] param_formats SQL format data to set + * @param param_length number of entries available in the @a param_values, @a param_lengths and @a param_formats arrays + * @param[out] scratch buffer for dynamic allocations (to be done via GNUNET_malloc() + * @param scratch_length number of entries left in @a scratch + * @return -1 on error, number of offsets used in @a scratch otherwise + */ +static int +qconv_amount_tuple (void *cls, + const void *data, + size_t data_len, + void *param_values[], + int param_lengths[], + int param_formats[], + unsigned int param_length, + void *scratch[], + unsigned int scratch_length) +{ + const struct GNUNET_PQ_Context *db = cls; + const struct TALER_Amount *amount = data; + size_t sz; + + GNUNET_assert (NULL != db); + GNUNET_assert (NULL != amount); + GNUNET_assert (1 == param_length); + GNUNET_assert (1 <= scratch_length); + GNUNET_assert (sizeof (struct TALER_Amount) == data_len); + GNUNET_static_assert (sizeof(uint32_t) == sizeof(Oid)); + + { + char *out; + struct TALER_PQ_Amount_P d = MAKE_TALER_PQ_AMOUNT_P (db, amount); + + sz = sizeof(uint32_t); /* number of elements in tuple */ + sz += sizeof(d); + + out = GNUNET_malloc (sz); + scratch[0] = out; + + *(uint32_t *) out = htonl (2); + out += sizeof(uint32_t); + + *(struct TALER_PQ_Amount_P*) out = d; + + } + + param_values[0] = scratch[0]; + param_lengths[0] = sz; + param_formats[0] = 1; + + return 1; +} + + +struct GNUNET_PQ_QueryParam +TALER_PQ_query_param_amount_tuple ( + const struct GNUNET_PQ_Context *db, + const struct TALER_Amount *amount) +{ + struct GNUNET_PQ_QueryParam res = { + .conv_cls = (void *) db, + .conv = &qconv_amount_tuple, + .data = amount, + .size = sizeof (*amount), + .num_params = 1, + }; + + return res; +} + + /** * Function called to convert input argument into SQL parameters. * @@ -793,7 +872,7 @@ qconv_array ( RETURN_UNLESS ((0 == num) || (y / num == x)); /* size of header */ - total_size = x = sizeof(struct TALER_PQ_ArrayHeader); + total_size = x = sizeof(struct TALER_PQ_ArrayHeader_P); total_size += y; RETURN_UNLESS (total_size >= x); @@ -862,7 +941,7 @@ qconv_array ( /* Write data */ { char *out = elements; - struct TALER_PQ_ArrayHeader h = { + struct TALER_PQ_ArrayHeader_P h = { .ndim = htonl (1), /* We only support one-dimensional arrays */ .has_null = htonl (0), /* We do not support NULL entries in arrays */ .lbound = htonl (1), /* Default start index value */ diff --git a/src/pq/pq_result_helper.c b/src/pq/pq_result_helper.c index 0a734bbc9..3ef2e71fb 100644 --- a/src/pq/pq_result_helper.c +++ b/src/pq/pq_result_helper.c @@ -264,6 +264,145 @@ TALER_PQ_result_spec_amount (const char *name, } +/** + * Extract an amount from a tuple from a Postgres database @a result at row @a row. + * + * @param cls closure, a `const char *` giving the currency + * @param result where to extract data from + * @param row row to extract data from + * @param fname name (or prefix) of the fields to extract from + * @param[in,out] dst_size where to store size of result, may be NULL + * @param[out] dst where to store the result + * @return + * #GNUNET_YES if all results could be extracted + * #GNUNET_NO if at least one result was NULL + * #GNUNET_SYSERR if a result was invalid (non-existing field) + */ +static enum GNUNET_GenericReturnValue +extract_amount_tuple (void *cls, + PGresult *result, + int row, + const char *fname, + size_t *dst_size, + void *dst) +{ + struct TALER_Amount *r_amount = dst; + const char *currency = cls; + int col; + int len; + + if (sizeof (struct TALER_Amount) != *dst_size) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + + /* Set return value to invalid in case we don't finish */ + memset (r_amount, + 0, + sizeof (struct TALER_Amount)); + col = PQfnumber (result, + fname); + if (col < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Field `%s' does not exist in result\n", + fname); + return GNUNET_SYSERR; + } + if (PQgetisnull (result, + row, + col)) + { + return GNUNET_NO; + } + + /* Parse the tuple */ + { + char *in; + uint32_t num; + struct TALER_PQ_Amount_P ap; + int size; + const static int expected_size = sizeof(uint32_t) /* length */ + + sizeof(struct TALER_PQ_Amount_P); + + size = PQgetlength (result, + row, + col); + + if (expected_size != size) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Incorrect size of binary field `%s' (got %d, expected %d)\n", + fname, + size, + expected_size); + return GNUNET_SYSERR; + } + + in = PQgetvalue (result, + row, + col); + + num = ntohl (*(uint32_t *) in); + if (2 != num) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Incorrect number of elements in tuple-field `%s'\n", + fname); + return GNUNET_SYSERR; + } + in += sizeof(uint32_t); + ap = *(struct TALER_PQ_Amount_P *) in; + + /* TODO[oec]: OID-checks? */ + + r_amount->value = GNUNET_ntohll (ap.v); + r_amount->fraction = ntohl (ap.f); + } + + if (r_amount->value >= TALER_AMOUNT_MAX_VALUE) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Value in field `%s' exceeds legal range\n", + fname); + return GNUNET_SYSERR; + } + if (r_amount->fraction >= TALER_AMOUNT_FRAC_BASE) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Fraction in field `%s' exceeds legal range\n", + fname); + return GNUNET_SYSERR; + } + + len = GNUNET_MIN (TALER_CURRENCY_LEN - 1, + strlen (currency)); + + GNUNET_memcpy (r_amount->currency, + currency, + len); + return GNUNET_OK; +} + + +struct GNUNET_PQ_ResultSpec +TALER_PQ_result_spec_amount_tuple (const char *name, + const char *currency, + struct TALER_Amount *amount) +{ + struct GNUNET_PQ_ResultSpec res = { + .conv = &extract_amount_tuple, + .cls = (void *) currency, + .dst = (void *) amount, + .dst_size = sizeof (*amount), + .fname = name + }; + + return res; +} + + /** * Extract data from a Postgres database @a result at row @a row. * @@ -1027,7 +1166,7 @@ extract_array_generic ( int data_sz; char *data; void *out = NULL; - struct TALER_PQ_ArrayHeader header; + struct TALER_PQ_ArrayHeader_P header; int col_num; GNUNET_assert (NULL != dst); @@ -1053,8 +1192,8 @@ extract_array_generic ( FAIL_IF (NULL == data); { - struct TALER_PQ_ArrayHeader *h = - (struct TALER_PQ_ArrayHeader *) data; + struct TALER_PQ_ArrayHeader_P *h = + (struct TALER_PQ_ArrayHeader_P *) data; header.ndim = ntohl (h->ndim); header.has_null = ntohl (h->has_null);