/* Copyright (c) 2023, The Tor Project, Inc. */
/* See LICENSE for licensing information */

/**
 * \file conflux_params.h
 * \brief Header file for conflux_params.c.
 **/

#include "core/or/or.h"

#include "app/config/config.h"

#include "core/or/conflux_params.h"
#include "core/or/congestion_control_common.h"
#include "core/or/circuitlist.h"

#include "feature/nodelist/networkstatus.h"
#include "feature/nodelist/networkstatus_st.h"
#include "feature/nodelist/routerstatus_st.h"
#include "feature/relay/routermode.h"

#include "core/or/origin_circuit_st.h"

/**
 * Consensus parameters defaults, minimums and maximums.
 */

/* For "cfx_enabled". */
#define CONFLUX_ENABLED_MIN (0)
#define CONFLUX_ENABLED_MAX (1)
#define CONFLUX_ENABLED_DEFAULT (1)

/* For "cfx_low_exit_threshold". This is a percentage scaled to 10000 so we can
 * support two decimal points. For example, 65.78% would be 6578. */
#define LOW_EXIT_THRESHOLD_MIN (0)
#define LOW_EXIT_THRESHOLD_MAX (10000)
#define LOW_EXIT_THRESHOLD_DEFAULT (6000)

/* For "cfx_max_linked_set". */
#define MAX_LINKED_SET_MIN (0)
#define MAX_LINKED_SET_MAX (UINT8_MAX)
#define MAX_LINKED_SET_DEFAULT (10)

/* For "cfx_max_prebuilt_set". */
#define MAX_PREBUILT_SET_MIN (0)
#define MAX_PREBUILT_SET_MAX (UINT8_MAX)
#define MAX_PREBUILT_SET_DEFAULT (3)

/* For "cfx_max_leg_retry". */
#define MAX_UNLINKED_LEG_RETRY_DEFAULT (3)
#define MAX_UNLINKED_LEG_RETRY_MIN (0)
#define MAX_UNLINKED_LEG_RETRY_MAX (UINT8_MAX)

/* For "cfx_num_legs_set". */
#define NUM_LEGS_SET_MIN (0)
#define NUM_LEGS_SET_MAX (UINT8_MAX)
#define NUM_LEGS_SET_DEFAULT (2)

/* For "cfx_send_pct". */
#define CFX_SEND_PCT_MIN (0)
#define CFX_SEND_PCT_MAX (255)
#define CFX_SEND_PCT_DFLT 100

/* For "cfx_drain_pct". */
#define CFX_DRAIN_PCT_MIN (0)
#define CFX_DRAIN_PCT_MAX (255)
#define CFX_DRAIN_PCT_DFLT 0

/*
 * Cached consensus parameters.
 */

/* Indicate if conflux is enabled or disabled. */
static bool conflux_enabled = CONFLUX_ENABLED_DEFAULT;
/* Maximum number of linked set we are allowed to have (even if in use). */
static uint8_t max_linked_set = MAX_LINKED_SET_DEFAULT;
/* Maximum number of pre built set. */
static uint8_t max_prebuilt_set = MAX_PREBUILT_SET_DEFAULT;
/* Maximum number of unlinked leg retry that is how many times are we allowed
 * to retry a leg until it successfully links. */
STATIC uint32_t max_unlinked_leg_retry = MAX_UNLINKED_LEG_RETRY_DEFAULT;
/* Number of legs per set. */
static uint8_t num_legs_set = NUM_LEGS_SET_DEFAULT;
/* The low Exit relay threshold, as a ratio between 0 and 1, used as a limit to
 * decide the amount of pre-built set we build depending on how many Exit relay
 * supports conflux in our current consensus. */
static double low_exit_threshold_ratio =
  LOW_EXIT_THRESHOLD_DEFAULT / (double)LOW_EXIT_THRESHOLD_MAX;

static uint8_t cfx_drain_pct = CFX_DRAIN_PCT_DFLT;
static uint8_t cfx_send_pct = CFX_SEND_PCT_DFLT;

/* Ratio of Exit relays in our consensus supporting conflux. This is computed
 * at every consensus and it is between 0 and 1. */
static double exit_conflux_ratio = 0.0;

/** Sets num_conflux_exit with the latest count of Exits in the given consensus
 * that supports Conflux. */
static void
count_exit_with_conflux_support(const networkstatus_t *ns)
{
  double supported = 0.0;
  int total_exits = 0;

  if (!ns || smartlist_len(ns->routerstatus_list) == 0) {
    return;
  }

  SMARTLIST_FOREACH_BEGIN(ns->routerstatus_list, const routerstatus_t *, rs) {
    if (!rs->is_exit || rs->is_bad_exit) {
      continue;
    }
    if (rs->pv.supports_conflux) {
      supported++;
    }
    total_exits++;
  } SMARTLIST_FOREACH_END(rs);

  if (total_exits > 0) {
    exit_conflux_ratio =
      supported / total_exits;
  } else {
    exit_conflux_ratio = 0.0;
  }

  log_info(LD_GENERAL, "Consensus has %.2f %% Exit relays supporting Conflux",
           exit_conflux_ratio * 100.0);
}

/**
 * Return true iff conflux feature is enabled and usable for a given circuit.
 *
 * Circ may be NULL, in which case we only check the consensus and torrc. */
bool
conflux_is_enabled(const circuit_t *circ)
{
  const or_options_t *opts = get_options();

  /* Conflux CAN NOT operate properly without congestion control and so
   * automatically disabled conflux if we don't have CC enabled. */
  if (!congestion_control_enabled()) {
    return false;
  }

  if (circ) {
    /* If circuit is non-null, we need to check to see if congestion
     * control was successfully negotiated. Conflux depends upon congestion
     * control, and consensus checks are not enough because there can be a
     * race between those checks and the consensus update to enable
     * congestion control. This happens in Shadow, and at relay restart. */
    if (CIRCUIT_IS_ORIGIN(circ)) {
      tor_assert(CONST_TO_ORIGIN_CIRCUIT(circ)->cpath);
      tor_assert(CONST_TO_ORIGIN_CIRCUIT(circ)->cpath->prev);
      if (!CONST_TO_ORIGIN_CIRCUIT(circ)->cpath->prev->ccontrol)
        return false;
    } else {
      if (!circ->ccontrol)
        return false;
    }
  }

  /* For clients, this is mostly for sbws. For relays, this is an emergency
   * emergency override, in case a bug is discovered by a relay operator
   * and we can't set a consensus param fast enough. Basically gives them
   * an option other than downgrading. */
  if (opts->ConfluxEnabled != -1) {
    if (server_mode(opts)) {
      char *msg;
      static ratelim_t rlimit = RATELIM_INIT(60 * 60); /* Hourly */
      if ((msg = rate_limit_log(&rlimit, time(NULL)))) {
        log_warn(LD_GENERAL,
                 "This tor is a relay and ConfluxEnabled is set to 0. "
                 "We would ask you to please write to us on "
                 "tor-relay@lists.torproject.org or file a bug explaining "
                 "why you have disabled this option. Without news from you, "
                 "we might end up marking your relay as a BadExit.");
        tor_free(msg);
      }
    }
    return opts->ConfluxEnabled;
  }

  return conflux_enabled;
}

/** Return the maximum number of linked set we are allowed to have. */
uint8_t
conflux_params_get_max_linked_set(void)
{
  return max_linked_set;
}

/** Return the number of maximum pre built sets that is allowed to have. */
uint8_t
conflux_params_get_max_prebuilt(void)
{
  /* Without any Exit supporting conflux, we won't be able to build a set. The
   * float problem here is minimal because exit_conflux_ratio is either a flat
   * 0 or else it means we do have at least an exit. */
  if (exit_conflux_ratio <= 0.0) {
    return 0;
  }

  /* Allow only 1 pre built set if we are lower than the low exit threshold
   * parameter from the consensus. */
  if (exit_conflux_ratio < low_exit_threshold_ratio) {
    return 1;
  }
  return max_prebuilt_set;
}

/** Return the maximum number of retry we can do until a leg links. */
uint8_t
conflux_params_get_max_unlinked_leg_retry(void)
{
  return max_unlinked_leg_retry;
}

/** Return the number of legs per set. */
uint8_t
conflux_params_get_num_legs_set(void)
{
  return num_legs_set;
}

/** Return the drain percent we must hit before switching */
uint8_t
conflux_params_get_drain_pct(void)
{
  return cfx_drain_pct;
}

/** Return the percent of the congestion window to send before switching. */
uint8_t
conflux_params_get_send_pct(void)
{
  return cfx_send_pct;
}

/** Update global conflux related consensus parameter values, every consensus
 * update. */
void
conflux_params_new_consensus(const networkstatus_t *ns)
{
  /* Params used by conflux_pool.c */
  conflux_enabled =
    networkstatus_get_param(ns, "cfx_enabled",
                            CONFLUX_ENABLED_DEFAULT,
                            CONFLUX_ENABLED_MIN, CONFLUX_ENABLED_MAX);

  low_exit_threshold_ratio =
    networkstatus_get_param(ns, "cfx_low_exit_threshold",
                            LOW_EXIT_THRESHOLD_DEFAULT,
                            LOW_EXIT_THRESHOLD_MIN, LOW_EXIT_THRESHOLD_MAX) /
    (double)LOW_EXIT_THRESHOLD_MAX;

  max_linked_set =
    networkstatus_get_param(ns, "cfx_max_linked_set",
                            MAX_LINKED_SET_DEFAULT,
                            MAX_LINKED_SET_MIN, MAX_LINKED_SET_MAX);

  max_prebuilt_set =
    networkstatus_get_param(ns, "cfx_max_prebuilt_set",
                            MAX_PREBUILT_SET_DEFAULT,
                            MAX_PREBUILT_SET_MIN, MAX_PREBUILT_SET_MAX);

  max_unlinked_leg_retry =
    networkstatus_get_param(ns, "cfx_max_unlinked_leg_retry",
                            MAX_UNLINKED_LEG_RETRY_DEFAULT,
                            MAX_UNLINKED_LEG_RETRY_MIN,
                            MAX_UNLINKED_LEG_RETRY_MAX);

  num_legs_set =
    networkstatus_get_param(ns, "cfx_num_legs_set",
                            NUM_LEGS_SET_DEFAULT,
                            NUM_LEGS_SET_MIN, NUM_LEGS_SET_MAX);

  /* Params used by conflux.c */
  cfx_send_pct = networkstatus_get_param(ns, "cfx_send_pct",
      CFX_SEND_PCT_DFLT,
      CFX_SEND_PCT_MIN,
      CFX_SEND_PCT_MAX);

  cfx_drain_pct = networkstatus_get_param(ns, "cfx_drain_pct",
      CFX_DRAIN_PCT_DFLT,
      CFX_DRAIN_PCT_MIN,
      CFX_DRAIN_PCT_MAX);

  count_exit_with_conflux_support(ns);
}
