expose 2^52 amount value limit in header, check that limit in test cases, add TALER_amount_multiply and TALER_amount_divide2 operations

This commit is contained in:
Christian Grothoff 2021-07-10 14:52:59 +02:00
parent 883b1fc70b
commit 249ba03c36
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
4 changed files with 213 additions and 166 deletions

6
debian/changelog vendored
View File

@ -1,3 +1,9 @@
taler-exchange (0.9.0-12) unstable; urgency=low
* Fix typo in taler-auditor shell script: clean before building.
-- Christian Grothoff <grothoff@gnu.org> Mon, 28 Jun 2021 14:02:10 +0100
taler-exchange (0.9.0-11) unstable; urgency=low taler-exchange (0.9.0-11) unstable; urgency=low
* Fix typo in taler-auditor-sync. * Fix typo in taler-auditor-sync.

View File

@ -64,6 +64,11 @@ extern "C"
*/ */
#define TALER_AMOUNT_FRAC_LEN 8 #define TALER_AMOUNT_FRAC_LEN 8
/**
* Maximum legal 'value' for an amount, based on IEEE double (for JavaScript compatibility).
*/
#define TALER_AMOUNT_MAX_VALUE (1LLU << 52)
GNUNET_NETWORK_STRUCT_BEGIN GNUNET_NETWORK_STRUCT_BEGIN
@ -332,6 +337,32 @@ TALER_amount_divide (struct TALER_Amount *result,
const struct TALER_Amount *dividend, const struct TALER_Amount *dividend,
uint32_t divisor); uint32_t divisor);
/**
* Divide one amount by another. Note that this function
* may introduce a rounding error. It rounds down.
*
* @param dividend amount to divide
* @param divisor by what to divide, must be positive
* @return @a dividend / @a divisor, rounded down. -1 on currency missmatch,
* INT_MAX for division by zero
*/
int
TALER_amount_divide2 (const struct TALER_Amount *dividend,
const struct TALER_Amount *divisor);
/**
* Multiply an @a amount by a @ factor.
*
* @param[out] result where to store @a amount * @a factor
* @param amount amount to multiply
* @param factor factor by which to multiply
*/
enum TALER_AmountArithmeticResult
TALER_amount_multiply (struct TALER_Amount *result,
const struct TALER_Amount *amount,
uint32_t factor);
/** /**
* Normalize the given amount. * Normalize the given amount.

View File

@ -1,6 +1,6 @@
/* /*
This file is part of TALER This file is part of TALER
Copyright (C) 2014 Taler Systems SA Copyright (C) 2014-2021 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the 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 terms of the GNU General Public License as published by the Free Software
@ -24,11 +24,6 @@
#include "platform.h" #include "platform.h"
#include "taler_util.h" #include "taler_util.h"
/**
* Maximum legal 'value' for an amount, based on IEEE double (for JavaScript compatibility).
*/
#define MAX_AMOUNT_VALUE (1LLU << 52)
/** /**
* Set @a a to "invalid". * Set @a a to "invalid".
@ -44,14 +39,6 @@ invalidate (struct TALER_Amount *a)
} }
/**
* Parse monetary amount, in the format "T:V.F".
*
* @param str amount string
* @param[out] amount amount to write the result to
* @return #GNUNET_OK if the string is a valid monetary amount specification,
* #GNUNET_SYSERR if it is invalid.
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_string_to_amount (const char *str, TALER_string_to_amount (const char *str,
struct TALER_Amount *amount) struct TALER_Amount *amount)
@ -128,8 +115,8 @@ TALER_string_to_amount (const char *str,
n = *value - '0'; n = *value - '0';
if ( (amount->value * 10 < amount->value) || if ( (amount->value * 10 < amount->value) ||
(amount->value * 10 + n < amount->value) || (amount->value * 10 + n < amount->value) ||
(amount->value > MAX_AMOUNT_VALUE) || (amount->value > TALER_AMOUNT_MAX_VALUE) ||
(amount->value * 10 + n > MAX_AMOUNT_VALUE) ) (amount->value * 10 + n > TALER_AMOUNT_MAX_VALUE) )
{ {
GNUNET_log (GNUNET_ERROR_TYPE_WARNING, GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Value specified in amount `%s' is too large\n", "Value specified in amount `%s' is too large\n",
@ -182,15 +169,6 @@ TALER_string_to_amount (const char *str,
} }
/**
* Parse monetary amount, in the format "T:V.F".
* The result is stored in network byte order (NBO).
*
* @param str amount string
* @param[out] amount_nbo amount to write the result to
* @return #GNUNET_OK if the string is a valid amount specification,
* #GNUNET_SYSERR if it is invalid.
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_string_to_amount_nbo (const char *str, TALER_string_to_amount_nbo (const char *str,
struct TALER_AmountNBO *amount_nbo) struct TALER_AmountNBO *amount_nbo)
@ -207,12 +185,6 @@ TALER_string_to_amount_nbo (const char *str,
} }
/**
* Convert amount from host to network representation.
*
* @param res where to store amount in network representation
* @param[out] d amount in host representation
*/
void void
TALER_amount_hton (struct TALER_AmountNBO *res, TALER_amount_hton (struct TALER_AmountNBO *res,
const struct TALER_Amount *d) const struct TALER_Amount *d)
@ -227,12 +199,6 @@ TALER_amount_hton (struct TALER_AmountNBO *res,
} }
/**
* Convert amount from network to host representation.
*
* @param[out] res where to store amount in host representation
* @param dn amount in network representation
*/
void void
TALER_amount_ntoh (struct TALER_Amount *res, TALER_amount_ntoh (struct TALER_Amount *res,
const struct TALER_AmountNBO *dn) const struct TALER_AmountNBO *dn)
@ -247,14 +213,6 @@ TALER_amount_ntoh (struct TALER_Amount *res,
} }
/**
* Get the value of "zero" in a particular currency.
*
* @param cur currency description
* @param[out] amount amount to write the result to
* @return #GNUNET_OK if @a cur is a valid currency specification,
* #GNUNET_SYSERR if it is invalid.
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_get_zero (const char *cur, TALER_amount_get_zero (const char *cur,
struct TALER_Amount *amount) struct TALER_Amount *amount)
@ -274,15 +232,11 @@ TALER_amount_get_zero (const char *cur,
} }
/**
* Test if the given amount is valid.
*
* @param amount amount to check
* @return #GNUNET_OK if @a amount is valid
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_is_valid (const struct TALER_Amount *amount) TALER_amount_is_valid (const struct TALER_Amount *amount)
{ {
if (amount->value > TALER_AMOUNT_MAX_VALUE)
return GNUNET_SYSERR;
return ('\0' != amount->currency[0]) ? GNUNET_OK : GNUNET_NO; return ('\0' != amount->currency[0]) ? GNUNET_OK : GNUNET_NO;
} }
@ -301,15 +255,6 @@ test_valid_nbo (const struct TALER_AmountNBO *a)
} }
/**
* Test if @a a1 and @a a2 are the same currency.
*
* @param a1 amount to test
* @param a2 amount to test
* @return #GNUNET_YES if @a a1 and @a a2 are the same currency
* #GNUNET_NO if the currencies are different,
* #GNUNET_SYSERR if either amount is invalid
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_cmp_currency (const struct TALER_Amount *a1, TALER_amount_cmp_currency (const struct TALER_Amount *a1,
const struct TALER_Amount *a2) const struct TALER_Amount *a2)
@ -324,15 +269,6 @@ TALER_amount_cmp_currency (const struct TALER_Amount *a1,
} }
/**
* Test if @a a1 and @a a2 are the same currency, NBO variant.
*
* @param a1 amount to test
* @param a2 amount to test
* @return #GNUNET_YES if @a a1 and @a a2 are the same currency
* #GNUNET_NO if the currencies are different,
* #GNUNET_SYSERR if either amount is invalid
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_cmp_currency_nbo (const struct TALER_AmountNBO *a1, TALER_amount_cmp_currency_nbo (const struct TALER_AmountNBO *a1,
const struct TALER_AmountNBO *a2) const struct TALER_AmountNBO *a2)
@ -347,19 +283,6 @@ TALER_amount_cmp_currency_nbo (const struct TALER_AmountNBO *a1,
} }
/**
* Compare the value/fraction of two amounts. Does not compare the currency.
* Comparing amounts of different currencies will cause the program to abort().
* If unsure, check with #TALER_amount_cmp_currency() first to be sure that
* the currencies of the two amounts are identical.
*
* @param a1 first amount
* @param a2 second amount
* @return result of the comparison,
* -1 if `a1 < a2`
* 1 if `a1 > a2`
* 0 if `a1 == a2`.
*/
int int
TALER_amount_cmp (const struct TALER_Amount *a1, TALER_amount_cmp (const struct TALER_Amount *a1,
const struct TALER_Amount *a2) const struct TALER_Amount *a2)
@ -390,19 +313,6 @@ TALER_amount_cmp (const struct TALER_Amount *a1,
} }
/**
* Compare the value/fraction of two amounts. Does not compare the currency.
* Comparing amounts of different currencies will cause the program to abort().
* If unsure, check with #TALER_amount_cmp_currency() first to be sure that
* the currencies of the two amounts are identical. NBO variant.
*
* @param a1 first amount
* @param a2 second amount
* @return result of the comparison
* -1 if `a1 < a2`
* 1 if `a1 > a2`
* 0 if `a1 == a2`.
*/
int int
TALER_amount_cmp_nbo (const struct TALER_AmountNBO *a1, TALER_amount_cmp_nbo (const struct TALER_AmountNBO *a1,
const struct TALER_AmountNBO *a2) const struct TALER_AmountNBO *a2)
@ -419,14 +329,6 @@ TALER_amount_cmp_nbo (const struct TALER_AmountNBO *a1,
} }
/**
* Perform saturating subtraction of amounts.
*
* @param[out] diff where to store (@a a1 - @a a2), or invalid if @a a2 > @a a1
* @param a1 amount to subtract from
* @param a2 amount to subtract
* @return operation status, negative on failures
*/
enum TALER_AmountArithmeticResult enum TALER_AmountArithmeticResult
TALER_amount_subtract (struct TALER_Amount *diff, TALER_amount_subtract (struct TALER_Amount *diff,
const struct TALER_Amount *a1, const struct TALER_Amount *a1,
@ -482,14 +384,6 @@ TALER_amount_subtract (struct TALER_Amount *diff,
} }
/**
* Perform addition of amounts.
*
* @param[out] sum where to store @a a1 + @a a2, set to "invalid" on overflow
* @param a1 first amount to add
* @param a2 second amount to add
* @return operation status, negative on failures
*/
enum TALER_AmountArithmeticResult enum TALER_AmountArithmeticResult
TALER_amount_add (struct TALER_Amount *sum, TALER_amount_add (struct TALER_Amount *sum,
const struct TALER_Amount *a1, const struct TALER_Amount *a1,
@ -500,7 +394,8 @@ TALER_amount_add (struct TALER_Amount *sum,
struct TALER_Amount res; struct TALER_Amount res;
if (GNUNET_YES != if (GNUNET_YES !=
TALER_amount_cmp_currency (a1, a2)) TALER_amount_cmp_currency (a1,
a2))
{ {
invalidate (sum); invalidate (sum);
return TALER_AAR_INVALID_CURRENCIES_INCOMPATIBLE; return TALER_AAR_INVALID_CURRENCIES_INCOMPATIBLE;
@ -509,8 +404,10 @@ TALER_amount_add (struct TALER_Amount *sum,
diff and a1/a2 */ diff and a1/a2 */
n1 = *a1; n1 = *a1;
n2 = *a2; n2 = *a2;
if ( (GNUNET_SYSERR == TALER_amount_normalize (&n1)) || if ( (GNUNET_SYSERR ==
(GNUNET_SYSERR == TALER_amount_normalize (&n2)) ) TALER_amount_normalize (&n1)) ||
(GNUNET_SYSERR ==
TALER_amount_normalize (&n2)) )
{ {
invalidate (sum); invalidate (sum);
return TALER_AAR_INVALID_NORMALIZATION_FAILED; return TALER_AAR_INVALID_NORMALIZATION_FAILED;
@ -526,7 +423,7 @@ TALER_amount_add (struct TALER_Amount *sum,
invalidate (sum); invalidate (sum);
return TALER_AAR_INVALID_RESULT_OVERFLOW; return TALER_AAR_INVALID_RESULT_OVERFLOW;
} }
if (res.value > MAX_AMOUNT_VALUE) if (res.value > TALER_AMOUNT_MAX_VALUE)
{ {
/* too large to be legal */ /* too large to be legal */
invalidate (sum); invalidate (sum);
@ -548,14 +445,6 @@ TALER_amount_add (struct TALER_Amount *sum,
} }
/**
* Normalize the given amount.
*
* @param[in,out] amount amount to normalize
* @return #GNUNET_OK if normalization worked
* #GNUNET_NO if value was already normalized
* #GNUNET_SYSERR if value was invalid or could not be normalized
*/
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_normalize (struct TALER_Amount *amount) TALER_amount_normalize (struct TALER_Amount *amount)
{ {
@ -569,7 +458,7 @@ TALER_amount_normalize (struct TALER_Amount *amount)
amount->fraction %= TALER_AMOUNT_FRAC_BASE; amount->fraction %= TALER_AMOUNT_FRAC_BASE;
amount->value += overflow; amount->value += overflow;
if ( (amount->value < overflow) || if ( (amount->value < overflow) ||
(amount->value > MAX_AMOUNT_VALUE) ) (amount->value > TALER_AMOUNT_MAX_VALUE) )
{ {
invalidate (amount); invalidate (amount);
return GNUNET_SYSERR; return GNUNET_SYSERR;
@ -600,12 +489,6 @@ amount_to_tail (const struct TALER_Amount *amount,
} }
/**
* Convert amount to string.
*
* @param amount amount to convert to string
* @return freshly allocated string representation
*/
char * char *
TALER_amount_to_string (const struct TALER_Amount *amount) TALER_amount_to_string (const struct TALER_Amount *amount)
{ {
@ -640,13 +523,6 @@ TALER_amount_to_string (const struct TALER_Amount *amount)
} }
/**
* Convert amount to string.
*
* @param amount amount to convert to string
* @return statically allocated buffer with string representation,
* NULL if the @a amount was invalid
*/
const char * const char *
TALER_amount2s (const struct TALER_Amount *amount) TALER_amount2s (const struct TALER_Amount *amount)
{ {
@ -685,14 +561,6 @@ TALER_amount2s (const struct TALER_Amount *amount)
} }
/**
* Divide an amount by a @a divisor. Note that this function
* may introduce a rounding error!
*
* @param[out] result where to store @a dividend / @a divisor
* @param dividend amount to divide
* @param divisor by what to divide, must be positive
*/
void void
TALER_amount_divide (struct TALER_Amount *result, TALER_amount_divide (struct TALER_Amount *result,
const struct TALER_Amount *dividend, const struct TALER_Amount *dividend,
@ -718,20 +586,109 @@ TALER_amount_divide (struct TALER_Amount *result,
} }
/** int
* Round the amount to something that can be transferred on the wire. TALER_amount_divide2 (const struct TALER_Amount *dividend,
* The rounding mode is specified via the smallest transferable unit, const struct TALER_Amount *divisor)
* which must only have a fractional part *or* only a value (either {
* of the two must be zero!). double approx;
* double d;
* If the @a round_unit given is zero, we do nothing and return #GNUNET_NO. double r;
* int ret;
* @param[in,out] amount amount to round down struct TALER_Amount tmp;
* @param[in] round_unit unit that should be rounded down to, and struct TALER_Amount nxt;
* either value part or the faction must be zero
* @return #GNUNET_OK on success, #GNUNET_NO if rounding was unnecessary, if (GNUNET_YES !=
* #GNUNET_SYSERR if the amount or currency or @a round_unit was invalid TALER_amount_cmp_currency (dividend,
*/ divisor))
{
GNUNET_break (0);
return -1;
}
if ( (0 == divisor->fraction) &&
(0 == divisor->value) )
return INT_MAX;
/* first, get rounded approximation */
d = ((double) dividend->value) * ((double) TALER_AMOUNT_FRAC_BASE)
+ ( (double) dividend->fraction);
r = ((double) divisor->value) * ((double) TALER_AMOUNT_FRAC_BASE)
+ ( (double) divisor->fraction);
approx = d / r;
if (approx > ((double) INT_MAX))
return INT_MAX; /* 'infinity' */
/* round down */
if (approx < 2)
ret = 0;
else
ret = (int) approx - 2;
/* Now do *exact* calculation, using well rounded-down factor as starting
point to avoid having to do too many steps. */
GNUNET_assert (0 <=
TALER_amount_multiply (&tmp,
divisor,
ret));
/* in practice, this loop will only run for one or two iterations */
while (1)
{
GNUNET_assert (0 <=
TALER_amount_add (&nxt,
&tmp,
divisor));
if (1 ==
TALER_amount_cmp (&nxt,
dividend))
break; /* nxt > dividend */
ret++;
tmp = nxt;
}
return ret;
}
enum TALER_AmountArithmeticResult
TALER_amount_multiply (struct TALER_Amount *result,
const struct TALER_Amount *amount,
uint32_t factor)
{
struct TALER_Amount in = *amount;
if (GNUNET_SYSERR ==
TALER_amount_normalize (&in))
return TALER_AAR_INVALID_NORMALIZATION_FAILED;
memcpy (result->currency,
amount->currency,
TALER_CURRENCY_LEN);
if ( (0 == factor) ||
( (0 == in.value) &&
(0 == in.fraction) ) )
{
result->value = 0;
result->fraction = 0;
return TALER_AAR_RESULT_ZERO;
}
result->value = in.value * ((uint64_t) factor);
if (in.value != result->value / factor)
return TALER_AAR_INVALID_RESULT_OVERFLOW;
{
/* This multiplication cannot overflow since both inputs are 32-bit values */
uint64_t tmp = ((uint64_t) factor) * ((uint64_t) in.fraction);
uint64_t res;
res = tmp / TALER_AMOUNT_FRAC_BASE;
/* check for overflow */
if (result->value + res < result->value)
return TALER_AAR_INVALID_RESULT_OVERFLOW;
result->value += res;
result->fraction = tmp % TALER_AMOUNT_FRAC_BASE;
}
if (result->value > TALER_AMOUNT_MAX_VALUE)
return TALER_AAR_INVALID_RESULT_OVERFLOW;
/* This check should be redundant... */
GNUNET_assert (GNUNET_SYSERR !=
TALER_amount_normalize (result));
return TALER_AAR_RESULT_POSITIVE;
}
enum GNUNET_GenericReturnValue enum GNUNET_GenericReturnValue
TALER_amount_round_down (struct TALER_Amount *amount, TALER_amount_round_down (struct TALER_Amount *amount,
const struct TALER_Amount *round_unit) const struct TALER_Amount *round_unit)

View File

@ -1,6 +1,6 @@
/* /*
This file is part of TALER This file is part of TALER
(C) 2015 Taler Systems SA (C) 2015, 2021 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the 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 terms of the GNU General Public License as published by the Free Software
@ -186,21 +186,27 @@ main (int argc,
/* test addition with overflow */ /* test addition with overflow */
a1.fraction = TALER_AMOUNT_FRAC_BASE - 1; a1.fraction = TALER_AMOUNT_FRAC_BASE - 1;
a1.value = UINT64_MAX - 5; a1.value = TALER_AMOUNT_MAX_VALUE - 5;
a2.fraction = 2; a2.fraction = 2;
a2.value = 5; a2.value = 5;
GNUNET_assert (TALER_AAR_INVALID_RESULT_OVERFLOW == GNUNET_assert (TALER_AAR_INVALID_RESULT_OVERFLOW ==
TALER_amount_add (&a3, &a1, &a2)); TALER_amount_add (&a3,
&a1,
&a2));
/* test addition with underflow on fraction */ /* test addition with underflow on fraction */
a1.fraction = 1; a1.fraction = 1;
a1.value = UINT64_MAX; a1.value = TALER_AMOUNT_MAX_VALUE;
a2.fraction = 2; a2.fraction = 2;
a2.value = 0; a2.value = 0;
GNUNET_assert (TALER_AAR_RESULT_POSITIVE == GNUNET_assert (TALER_AAR_RESULT_POSITIVE ==
TALER_amount_subtract (&a3, &a1, &a2)); TALER_amount_subtract (&a3,
GNUNET_assert (UINT64_MAX - 1 == a3.value); &a1,
GNUNET_assert (TALER_AMOUNT_FRAC_BASE - 1 == a3.fraction); &a2));
GNUNET_assert (TALER_AMOUNT_MAX_VALUE - 1 ==
a3.value);
GNUNET_assert (TALER_AMOUNT_FRAC_BASE - 1 ==
a3.fraction);
/* test division */ /* test division */
GNUNET_assert (GNUNET_OK == GNUNET_assert (GNUNET_OK ==
@ -288,6 +294,53 @@ main (int argc,
&r)); &r));
GNUNET_assert (0 == TALER_amount_cmp (&a1, GNUNET_assert (0 == TALER_amount_cmp (&a1,
&a2)); &a2));
/* test multiplication */
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:0",
&a1));
GNUNET_assert (TALER_AAR_RESULT_ZERO ==
TALER_amount_multiply (&a2,
&a1,
42));
GNUNET_assert (0 == TALER_amount_cmp (&a1,
&a2));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:5.001",
&a1));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:5001",
&r));
GNUNET_assert (TALER_AAR_RESULT_POSITIVE ==
TALER_amount_multiply (&a2,
&a1,
1000));
GNUNET_assert (0 == TALER_amount_cmp (&r,
&a2));
GNUNET_assert (1000 ==
TALER_amount_divide2 (&a2,
&a1));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:5006.00099999",
&r));
GNUNET_assert (1000 ==
TALER_amount_divide2 (&r,
&a1));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:5000.99999999",
&r));
GNUNET_assert (999 ==
TALER_amount_divide2 (&r,
&a1));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount ("BTC:0",
&a1));
GNUNET_assert (INT_MAX ==
TALER_amount_divide2 (&a2,
&a1));
GNUNET_assert (0 ==
TALER_amount_divide2 (&a1,
&a2));
return 0; return 0;
} }