/*
This file is part of TALER
Copyright (C) 2023 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
*/
/**
* @file lib/exchange_api_stefan.c
* @brief calculations on the STEFAN curve
* @author Christian Grothoff
*/
#include "platform.h"
#include "taler_json_lib.h"
#include
#include "exchange_api_handle.h"
#include
/**
* Determine smallest denomination in @a keys.
*
* @param keys exchange response to evaluate
* @return NULL on error (no denominations)
*/
static const struct TALER_Amount *
get_unit (const struct TALER_EXCHANGE_Keys *keys)
{
const struct TALER_Amount *min = NULL;
for (unsigned int i = 0; inum_denom_keys; i++)
{
const struct TALER_EXCHANGE_DenomPublicKey *dk
= &keys->denom_keys[i];
if ( (NULL == min) ||
(1 == TALER_amount_cmp (min,
/* > */
&dk->value)) )
min = &dk->value;
}
GNUNET_break (NULL != min);
return min;
}
/**
* Convert amount to double for STEFAN curve evaluation.
*
* @param a input amount
* @return (rounded) amount as a double
*/
static double
amount_to_double (const struct TALER_Amount *a)
{
double d = (double) a->value;
d += a->fraction / ((double) TALER_AMOUNT_FRAC_BASE);
return d;
}
/**
* Convert double to amount for STEFAN curve evaluation.
*
* @param dv input amount
* @param currency deisred currency
* @param[out] rval (rounded) amount as a double
*/
static void
double_to_amount (double dv,
const char *currency,
struct TALER_Amount *rval)
{
double rem;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (currency,
rval));
rval->value = floorl (dv);
rem = dv - ((double) rval->value);
if (rem < 0.0)
rem = 0.0;
rem *= TALER_AMOUNT_FRAC_BASE;
rval->fraction = floorl (rem);
if (rval->fraction >= TALER_AMOUNT_FRAC_BASE)
{
/* Strange, multiplication overflowed our range,
round up value instead */
rval->fraction = 0;
rval->value += 1;
}
}
enum GNUNET_GenericReturnValue
TALER_EXCHANGE_keys_stefan_b2n (
const struct TALER_EXCHANGE_Keys *keys,
const struct TALER_Amount *brut,
struct TALER_Amount *net)
{
const struct TALER_Amount *min;
double log_d = amount_to_double (&keys->stefan_log);
double lin_d = keys->stefan_lin;
double abs_d = amount_to_double (&keys->stefan_abs);
double bru_d = amount_to_double (brut);
double min_d;
double fee_d;
double net_d;
if (TALER_amount_is_zero (brut))
{
*net = *brut;
return GNUNET_NO;
}
min = get_unit (keys);
if (NULL == min)
return GNUNET_SYSERR;
if (1.0f <= keys->stefan_lin)
{
/* This cannot work, linear STEFAN fee estimate always
exceed any gross amount. */
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
min_d = amount_to_double (min);
fee_d = abs_d
+ log_d * log2 (bru_d / min_d)
+ lin_d * bru_d;
if (fee_d > bru_d)
{
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (brut->currency,
net));
return GNUNET_NO;
}
net_d = bru_d - fee_d;
double_to_amount (net_d,
brut->currency,
net);
return GNUNET_OK;
}
/**
* Our function
* f(x) := ne + ab + lo * log2(x/mi) + li * x - x
* for #newton().
*/
static double
eval_f (double mi,
double ab,
double lo,
double li,
double ne,
double x)
{
return ne + ab + lo * log2 (x / mi) + li * x - x;
}
/**
* Our function
* f'(x) := lo / log(2) / x + li - 1
* for #newton().
*/
static double
eval_fp (double mi,
double lo,
double li,
double ne,
double x)
{
return lo / log (2) / x + li - 1;
}
/**
* Use Newton's method to find x where f(x)=0.
*
* @return x where "eval_f(x)==0".
*/
static double
newton (double mi,
double ab,
double lo,
double li,
double ne)
{
const double eps = 0.00000001; /* max error allowed */
double min_ab = ne + ab; /* result cannot be smaller than this! */
/* compute lower bounds by various heuristics */
double min_ab_li = min_ab + min_ab * li;
double min_ab_li_lo = min_ab_li + log2 (min_ab_li / mi) * lo;
double min_ab_lo = min_ab + log2 (min_ab / mi) * lo;
double min_ab_lo_li = min_ab_lo + min_ab_lo * li;
/* take global lower bound */
double x_min = GNUNET_MAX (min_ab_lo_li,
min_ab_li_lo);
double x = x_min; /* use lower bound as starting point */
/* Objective: invert
ne := br - ab - lo * log2 (br/mi) - li * br
to find 'br'.
Method: use Newton's method to find root of:
f(x) := ne + ab + lo * log2 (x/mi) + li * x - x
using also
f'(x) := lo / log(2) / x + li - 1
*/
/* Loop to abort in case of divergence;
100 is already very high, 2-4 is normal! */
for (unsigned int i = 0; i<100; i++)
{
double fx = eval_f (mi, ab, lo, li, ne, x);
double fxp = eval_fp (mi, lo, li, ne, x);
double x_new = x - fx / fxp;
if (fabs (x - x_new) <= eps)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Needed %u rounds from %f to result BRUT %f => NET: %f\n",
i,
x_min,
x_new,
x_new - ab - li * x_new - lo * log2 (x / mi));
return x_new;
}
if (x_new < x_min)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Divergence, obtained very bad estimate %f after %u rounds!\n",
x_new,
i);
return x_min;
}
x = x_new;
}
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Slow convergence, returning bad estimate %f!\n",
x);
return x;
}
enum GNUNET_GenericReturnValue
TALER_EXCHANGE_keys_stefan_n2b (
const struct TALER_EXCHANGE_Keys *keys,
const struct TALER_Amount *net,
struct TALER_Amount *brut)
{
const struct TALER_Amount *min;
double lin_d = keys->stefan_lin;
double log_d = amount_to_double (&keys->stefan_log);
double abs_d = amount_to_double (&keys->stefan_abs);
double net_d = amount_to_double (net);
double min_d;
double brut_d;
if (TALER_amount_is_zero (net))
{
*brut = *net;
return GNUNET_NO;
}
min = get_unit (keys);
if (NULL == min)
return GNUNET_SYSERR;
if (1.0f <= keys->stefan_lin)
{
/* This cannot work, linear STEFAN fee estimate always
exceed any gross amount. */
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
min_d = amount_to_double (min);
brut_d = newton (min_d,
abs_d,
log_d,
lin_d,
net_d);
double_to_amount (brut_d,
net->currency,
brut);
return GNUNET_OK;
}
void
TALER_EXCHANGE_keys_stefan_round (
const struct TALER_EXCHANGE_Keys *keys,
struct TALER_Amount *val)
{
const struct TALER_Amount *min;
uint32_t mod;
uint32_t frac;
uint32_t lim;
if (0 == val->fraction)
{
/* rounding of non-fractions not supported */
return;
}
min = get_unit (keys);
if (NULL == min)
return;
if (0 == min->fraction)
{
frac = TALER_AMOUNT_FRAC_BASE;
}
else
{
frac = min->fraction;
}
lim = frac / 2;
mod = val->fraction % frac;
if (mod < lim)
val->fraction -= mod; /* round down */
else
val->fraction += frac - mod; /* round up */
}