use std::ops::Deref;
use crate::error::Error;
use bitcoin::Amount;
use dlc::{Payout, RangePayout};
#[cfg(feature = "use-serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct PayoutFunction {
pub(crate) payout_function_pieces: Vec<PayoutFunctionPiece>,
}
fn is_continuous(function_pieces: &[PayoutFunctionPiece]) -> bool {
function_pieces
.iter()
.zip(function_pieces.iter().skip(1))
.all(|(cur, next)| cur.get_last_point() == next.get_first_point())
}
impl PayoutFunction {
pub fn new(function_pieces: Vec<PayoutFunctionPiece>) -> Result<PayoutFunction, Error> {
if !function_pieces.is_empty() && is_continuous(&function_pieces) {
Ok(PayoutFunction {
payout_function_pieces: function_pieces,
})
} else {
Err(Error::InvalidParameters(
"Function pieces are not continuous.".to_string(),
))
}
}
pub fn validate(&self, max_value: u64) -> Result<(), Error> {
if !is_continuous(&self.payout_function_pieces) {
return Err(Error::InvalidParameters(
"Payout function is not continuous.".to_string(),
));
}
let covers = {
let first = self
.payout_function_pieces
.first()
.expect("to have at least one piece");
let starts_at_zero = match first {
PayoutFunctionPiece::PolynomialPayoutCurvePiece(p) => {
p.payout_points
.first()
.expect("to have at least a point")
.event_outcome
== 0
}
PayoutFunctionPiece::HyperbolaPayoutCurvePiece(h) => {
h.left_end_point.event_outcome == 0
}
};
let last = self
.payout_function_pieces
.last()
.expect("to have at least one piece");
let finishes_at_max = match last {
PayoutFunctionPiece::PolynomialPayoutCurvePiece(p) => {
p.payout_points
.last()
.expect("to have at least a point")
.event_outcome
== max_value
}
PayoutFunctionPiece::HyperbolaPayoutCurvePiece(h) => {
h.right_end_point.event_outcome == max_value
}
};
starts_at_zero && finishes_at_max
};
if covers {
Ok(())
} else {
Err(Error::InvalidParameters(
"Payout function doesn't cover all the possible outcomes.".to_string(),
))
}
}
pub fn to_range_payouts(
&self,
total_collateral: Amount,
rounding_intervals: &RoundingIntervals,
) -> Result<Vec<RangePayout>, Error> {
let mut range_payouts = Vec::new();
for piece in &self.payout_function_pieces {
piece.to_range_payouts(total_collateral, rounding_intervals, &mut range_payouts)?;
}
Ok(range_payouts)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub enum PayoutFunctionPiece {
PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece),
HyperbolaPayoutCurvePiece(HyperbolaPayoutCurvePiece),
}
impl PayoutFunctionPiece {
pub fn to_range_payouts(
&self,
total_collateral: Amount,
rounding_intervals: &RoundingIntervals,
range_payouts: &mut Vec<RangePayout>,
) -> Result<(), Error> {
match self {
PayoutFunctionPiece::PolynomialPayoutCurvePiece(p) => {
p.to_range_payouts(rounding_intervals, total_collateral, range_payouts)
}
PayoutFunctionPiece::HyperbolaPayoutCurvePiece(h) => {
h.to_range_payouts(rounding_intervals, total_collateral, range_payouts)
}
}
}
fn get_first_point(&self) -> &PayoutPoint {
match self {
PayoutFunctionPiece::PolynomialPayoutCurvePiece(p) => &p.payout_points[0],
PayoutFunctionPiece::HyperbolaPayoutCurvePiece(h) => &h.left_end_point,
}
}
fn get_last_point(&self) -> &PayoutPoint {
match self {
PayoutFunctionPiece::PolynomialPayoutCurvePiece(p) => p.payout_points.last().unwrap(),
PayoutFunctionPiece::HyperbolaPayoutCurvePiece(h) => &h.right_end_point,
}
}
}
trait Evaluable {
fn evaluate(&self, outcome: u64) -> f64;
fn get_rounded_payout(
&self,
outcome: u64,
rounding_intervals: &RoundingIntervals,
total_collateral: Amount,
) -> Result<Amount, Error> {
let payout_double = self.evaluate(outcome);
let total_collateral_sats = total_collateral.to_sat();
if payout_double.is_sign_negative() || (payout_double != 0.0 && !payout_double.is_normal())
{
return Err(Error::InvalidParameters(format!(
"Could not evaluate function for outcome {}, result was: {}",
outcome, payout_double
)));
}
if payout_double.round() > total_collateral_sats as f64 {
return Err(Error::InvalidParameters(
"Computed payout is greater than total collateral".to_string(),
));
}
Ok(Amount::from_sat(u64::min(
rounding_intervals.round(outcome, payout_double),
total_collateral_sats,
)))
}
fn get_first_outcome(&self) -> u64;
fn get_last_outcome(&self) -> u64;
fn get_cur_range(
&self,
range_payouts: &mut Vec<RangePayout>,
total_collateral: Amount,
rounding_intervals: &RoundingIntervals,
) -> Result<RangePayout, Error> {
let res = match range_payouts.pop() {
Some(cur) => cur,
None => {
let first_outcome = self.get_first_outcome();
let first_payout =
self.get_rounded_payout(first_outcome, rounding_intervals, total_collateral)?;
RangePayout {
start: first_outcome as usize,
count: 1,
payout: Payout {
offer: first_payout,
accept: total_collateral - first_payout,
},
}
}
};
Ok(res)
}
fn to_range_payouts(
&self,
rounding_intervals: &RoundingIntervals,
total_collateral: Amount,
range_payouts: &mut Vec<RangePayout>,
) -> Result<(), Error> {
compute_range_payouts(self, rounding_intervals, total_collateral, range_payouts)
}
}
fn compute_range_payouts<E: Deref>(
function: E,
rounding_intervals: &RoundingIntervals,
total_collateral: Amount,
range_payouts: &mut Vec<RangePayout>,
) -> Result<(), Error>
where
E::Target: Evaluable,
{
let first_outcome = function.get_first_outcome();
let mut cur_range =
function.get_cur_range(range_payouts, total_collateral, rounding_intervals)?;
let range_end = function.get_last_outcome().saturating_add(1);
for outcome in (first_outcome + 1)..range_end {
let payout = function.get_rounded_payout(outcome, rounding_intervals, total_collateral)?;
if payout > total_collateral {
return Err(Error::InvalidParameters(
"Computed payout is greater than total collateral.".to_string(),
));
}
if cur_range.payout.offer == payout {
cur_range.count += 1;
} else {
range_payouts.push(cur_range);
cur_range = RangePayout {
start: outcome as usize,
count: 1,
payout: Payout {
offer: payout,
accept: total_collateral - payout,
},
};
}
}
range_payouts.push(cur_range);
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct PolynomialPayoutCurvePiece {
pub(crate) payout_points: Vec<PayoutPoint>,
}
impl PolynomialPayoutCurvePiece {
pub fn new(payout_points: Vec<PayoutPoint>) -> Result<Self, Error> {
let is_ascending = payout_points.len() > 1
&& payout_points
.iter()
.zip(payout_points.iter().skip(1))
.all(|(cur, next)| cur.event_outcome < next.event_outcome);
if is_ascending {
Ok(PolynomialPayoutCurvePiece { payout_points })
} else {
Err(Error::InvalidParameters(
"Payout points must have ascending event outcome value.".to_string(),
))
}
}
}
impl Evaluable for PolynomialPayoutCurvePiece {
fn evaluate(&self, outcome: u64) -> f64 {
let nb_points = self.payout_points.len();
if nb_points == 2 {
let (left_point, right_point) = (&self.payout_points[0], &self.payout_points[1]);
let right_point_payout_sats = right_point.outcome_payout.to_sat() as f64;
let left_point_payout_sats = left_point.outcome_payout.to_sat() as f64;
return if left_point.outcome_payout == right_point.outcome_payout {
right_point_payout_sats
} else {
let slope = (right_point_payout_sats - left_point_payout_sats)
/ (right_point.event_outcome - left_point.event_outcome) as f64;
(outcome - left_point.event_outcome) as f64 * slope + left_point_payout_sats
};
}
let mut result = 0.0;
let outcome = outcome as f64;
for i in 0..nb_points {
let mut l = self.payout_points[i].get_outcome_payout();
for j in 0..nb_points {
if i != j {
debug_assert!(
self.payout_points[i].event_outcome != self.payout_points[j].event_outcome
);
let i_outcome = self.payout_points[i].event_outcome as f64;
let j_outcome = self.payout_points[j].event_outcome as f64;
let denominator = i_outcome - j_outcome;
let numerator = outcome - j_outcome;
l *= numerator / denominator;
}
}
result += l;
}
result
}
fn get_first_outcome(&self) -> u64 {
self.payout_points[0].event_outcome
}
fn get_last_outcome(&self) -> u64 {
self.payout_points.last().unwrap().event_outcome
}
fn to_range_payouts(
&self,
rounding_intervals: &RoundingIntervals,
total_collateral: Amount,
range_payouts: &mut Vec<RangePayout>,
) -> Result<(), Error> {
if self.payout_points.len() == 2
&& self.payout_points[0].outcome_payout == self.payout_points[1].outcome_payout
{
let mut cur_range =
self.get_cur_range(range_payouts, total_collateral, rounding_intervals)?;
cur_range.count += (self.payout_points[1].event_outcome
- self.payout_points[0].event_outcome) as usize;
range_payouts.push(cur_range);
return Ok(());
}
compute_range_payouts(self, rounding_intervals, total_collateral, range_payouts)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct PayoutPoint {
pub event_outcome: u64,
pub outcome_payout: Amount,
pub extra_precision: u16,
}
impl PayoutPoint {
fn get_outcome_payout(&self) -> f64 {
(self.outcome_payout.to_sat() as f64) + ((self.extra_precision as f64) / ((1 << 16) as f64))
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct HyperbolaPayoutCurvePiece {
pub(crate) left_end_point: PayoutPoint,
pub(crate) right_end_point: PayoutPoint,
pub(crate) use_positive_piece: bool,
pub(crate) translate_outcome: f64,
pub(crate) translate_payout: f64,
pub(crate) a: f64,
pub(crate) b: f64,
pub(crate) c: f64,
pub(crate) d: f64,
}
impl HyperbolaPayoutCurvePiece {
pub fn new(
left_end_point: PayoutPoint,
right_end_point: PayoutPoint,
use_positive_piece: bool,
translate_outcome: f64,
translate_payout: f64,
a: f64,
b: f64,
c: f64,
d: f64,
) -> Result<Self, Error> {
if a * d == b * c {
Err(Error::InvalidParameters(
"a * c cannot equal d * c".to_string(),
))
} else if left_end_point.event_outcome >= right_end_point.event_outcome {
Err(Error::InvalidParameters(
"Left end point outcome must be strictly less than right end point outcome"
.to_string(),
))
} else {
Ok(HyperbolaPayoutCurvePiece {
left_end_point,
right_end_point,
use_positive_piece,
translate_outcome,
translate_payout,
a,
b,
c,
d,
})
}
}
}
impl Evaluable for HyperbolaPayoutCurvePiece {
fn evaluate(&self, outcome: u64) -> f64 {
let outcome = outcome as f64;
let translated_outcome = outcome - self.translate_outcome;
let sqrt_term_abs_val = (translated_outcome.powi(2) - 4.0 * self.a * self.b).sqrt();
let sqrt_term = if self.use_positive_piece {
sqrt_term_abs_val
} else {
-sqrt_term_abs_val
};
let first_term = self.c * (translated_outcome + sqrt_term) / (2.0 * self.a);
let second_term = 2.0 * self.a * self.d / (translated_outcome + sqrt_term);
first_term + second_term + self.translate_payout
}
fn get_first_outcome(&self) -> u64 {
self.left_end_point.event_outcome
}
fn get_last_outcome(&self) -> u64 {
self.right_end_point.event_outcome
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct RoundingInterval {
pub begin_interval: u64,
pub rounding_mod: u64,
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "use-serde",
derive(Serialize, Deserialize),
serde(rename_all = "camelCase")
)]
pub struct RoundingIntervals {
pub intervals: Vec<RoundingInterval>,
}
impl RoundingIntervals {
pub fn round(&self, outcome: u64, payout: f64) -> u64 {
let rounding_mod = match self
.intervals
.binary_search_by(|x| x.begin_interval.cmp(&outcome))
{
Ok(index) => self.intervals[index].rounding_mod,
Err(index) if index != 0 => self.intervals[index - 1].rounding_mod,
_ => unreachable!(),
} as f64;
let m = if payout >= 0.0 {
payout % rounding_mod
} else {
payout % rounding_mod + rounding_mod
};
if m >= rounding_mod / 2.0 {
(payout + rounding_mod - m).round() as u64
} else {
(payout - m).round() as u64
}
}
pub fn validate(&self) -> Result<(), Error> {
if self.intervals.is_empty() {
return Err(Error::InvalidParameters(
"Empty rounding intervals.".to_string(),
));
}
if self.intervals[0].begin_interval != 0 {
return Err(Error::InvalidParameters(
"Rounding interval doesn't start at 0.".to_string(),
));
}
let mut cur = self.intervals[0].begin_interval;
if self.intervals.iter().skip(1).any(|x| {
let res = x.begin_interval <= cur;
cur = x.begin_interval;
res
}) {
return Err(Error::InvalidParameters(
"Rounding intervals being value are not strictly increasing".to_string(),
));
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use secp256k1_zkp::rand::{thread_rng, RngCore};
#[test]
fn lagrange_interpolate_test() {
let polynomial = PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::from_sat(1),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 2,
outcome_payout: Amount::from_sat(5),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 4,
outcome_payout: Amount::from_sat(17),
extra_precision: 0,
},
],
};
assert_eq!(101_f64, polynomial.evaluate(10));
assert_eq!(10001_f64, polynomial.evaluate(100));
}
#[test]
fn polynomial_to_range_outcome_test() {
struct TestCase {
payout_points: Vec<PayoutPoint>,
expected_len: usize,
expected_first_start: usize,
expected_first_payout: Amount,
expected_last_start: usize,
expected_last_payout: Amount,
total_collateral: Amount,
}
let test_cases: Vec<TestCase> = vec![
TestCase {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 20,
outcome_payout: Amount::from_sat(20),
extra_precision: 0,
},
],
expected_len: 21,
expected_first_start: 0,
expected_first_payout: Amount::ZERO,
expected_last_start: 20,
expected_last_payout: Amount::from_sat(20),
total_collateral: Amount::from_sat(20),
},
TestCase {
payout_points: vec![
PayoutPoint {
event_outcome: 10,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 20,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
],
expected_len: 1,
expected_first_start: 10,
expected_first_payout: Amount::from_sat(10),
expected_last_start: 10,
expected_last_payout: Amount::from_sat(10),
total_collateral: Amount::from_sat(10),
},
TestCase {
payout_points: vec![
PayoutPoint {
event_outcome: 50000,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 1048575,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
expected_len: 1,
expected_first_start: 50000,
expected_first_payout: Amount::ZERO,
expected_last_start: 50000,
expected_last_payout: Amount::ZERO,
total_collateral: Amount::from_sat(200000000),
},
];
for test_case in test_cases {
let polynomial = PolynomialPayoutCurvePiece {
payout_points: test_case.payout_points,
};
let rounding_intervals = RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod: 1,
}],
};
let mut range_payouts = Vec::new();
polynomial
.to_range_payouts(
&rounding_intervals,
test_case.total_collateral,
&mut range_payouts,
)
.expect("to be able to compute the range payouts");
let first = range_payouts.first().unwrap();
let last = range_payouts.last().unwrap();
assert_eq!(test_case.expected_len, range_payouts.len());
assert_eq!(test_case.expected_first_start, first.start);
assert_eq!(test_case.expected_first_payout, first.payout.offer);
assert_eq!(test_case.expected_last_start, last.start);
assert_eq!(test_case.expected_last_payout, last.payout.offer);
}
}
#[test]
fn hyperbola_evaluate_test() {
let d = (thread_rng().next_u64() as f64) + (thread_rng().next_u64() as f64 / 100.0);
let translate_payout =
(thread_rng().next_u64() as f64) + (thread_rng().next_u64() as f64 / 100.0);
let outcomes: Vec<_> = (0..100).map(|_| thread_rng().next_u64()).collect();
let expected_payout = |outcome: u64| -> f64 { d / outcome as f64 + translate_payout };
let hyperbola = HyperbolaPayoutCurvePiece {
left_end_point: PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
right_end_point: PayoutPoint {
event_outcome: u64::MAX,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
use_positive_piece: true,
translate_outcome: 0.0,
translate_payout,
a: 1.0,
b: 0.0,
c: 0.0,
d,
};
for outcome in outcomes {
assert_eq!(expected_payout(outcome), hyperbola.evaluate(outcome));
}
}
#[test]
fn hyperbola_to_range_outcome_invalid_curve_test() {
let hyperbola = HyperbolaPayoutCurvePiece {
left_end_point: PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
right_end_point: PayoutPoint {
event_outcome: 1000,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
use_positive_piece: true,
translate_outcome: 2.5,
translate_payout: 0.0,
a: 1.0,
b: -1.4,
c: -0.1,
d: 10.0,
};
hyperbola
.to_range_payouts(
&RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod: 1,
}],
},
Amount::from_sat(200000000),
&mut Vec::new(),
)
.expect_err("Should not tolerate negative payout");
}
#[test]
fn hyperbola_to_range_outcome_test() {
let hyperbola = HyperbolaPayoutCurvePiece {
left_end_point: PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
right_end_point: PayoutPoint {
event_outcome: 1000,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
use_positive_piece: true,
translate_outcome: 2.5,
translate_payout: 0.0,
a: 1.0,
b: -1.4,
c: 0.0,
d: 10.0,
};
hyperbola
.to_range_payouts(
&RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod: 1,
}],
},
Amount::from_sat(200000000),
&mut Vec::new(),
)
.expect("to be able to compute the range payouts");
}
#[test]
fn hyperbola_invalid_parameters_tests() {
HyperbolaPayoutCurvePiece::new(
PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 1000,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
true,
2.5,
0.0,
1.0,
1.0,
4.0,
4.0,
)
.expect_err("Should return error when a*d == b*c");
}
#[test]
fn payout_function_to_range_outcome_test() {
let payout_function = PayoutFunction::new(vec![
PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 9,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
])
.unwrap(),
),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 9,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 10,
outcome_payout: Amount::from_sat(9),
extra_precision: 0,
},
])
.unwrap(),
),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 10,
outcome_payout: Amount::from_sat(9),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 19,
outcome_payout: Amount::from_sat(9),
extra_precision: 0,
},
])
.unwrap(),
),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 19,
outcome_payout: Amount::from_sat(9),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 20,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
])
.unwrap(),
),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 20,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
PayoutPoint {
event_outcome: u64::MAX,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
])
.unwrap(),
),
])
.unwrap();
let expected_ranges = vec![
RangePayout {
start: 0,
count: 10,
payout: Payout {
offer: Amount::ZERO,
accept: Amount::from_sat(10),
},
},
RangePayout {
start: 10,
count: 10,
payout: Payout {
offer: Amount::from_sat(9),
accept: Amount::from_sat(1),
},
},
RangePayout {
start: 20,
count: (u64::MAX - 19) as usize,
payout: Payout {
offer: Amount::from_sat(10),
accept: Amount::ZERO,
},
},
];
assert_eq!(
expected_ranges,
payout_function
.to_range_payouts(
Amount::from_sat(10),
&RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod: 1
}]
}
)
.expect("to be able to compute the range payouts.")
);
}
#[test]
fn polynomial_payout_curve_validity_test() {
let invalid = vec![
vec![PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
}],
vec![
PayoutPoint {
event_outcome: 10,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 9,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
];
for points in invalid {
PolynomialPayoutCurvePiece::new(points).expect_err("Invalid pieces should error");
}
}
#[test]
fn hyperbola_validity_test() {
HyperbolaPayoutCurvePiece::new(
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
true,
0.0,
0.0,
1.0,
2.0,
3.0,
4.0,
)
.expect_err("Payout point are not increasing should error.");
HyperbolaPayoutCurvePiece::new(
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
true,
0.0,
0.0,
1.0,
2.0,
1.0,
2.0,
)
.expect_err("a * b == d * c should error.");
}
#[test]
fn payout_function_validity_test() {
let invalid = vec![
vec![
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 9,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
}),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 11,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 19,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
}),
],
vec![
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 9,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
}),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 10,
outcome_payout: Amount::from_sat(1),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 19,
outcome_payout: Amount::from_sat(1),
extra_precision: 0,
},
],
}),
],
];
for pieces in invalid {
PayoutFunction::new(pieces).expect_err("Invalid pieces should error");
}
}
#[test]
fn rounding_intervals_valid_is_valid() {
let rounding_intervals = RoundingIntervals {
intervals: vec![
RoundingInterval {
begin_interval: 0,
rounding_mod: 1,
},
RoundingInterval {
begin_interval: 10,
rounding_mod: 1,
},
],
};
rounding_intervals.validate().expect("it to be valid.");
}
#[test]
fn rounding_intervals_empty_is_invalid() {
let rounding_intervals = RoundingIntervals { intervals: vec![] };
rounding_intervals
.validate()
.expect_err("should not accept empty rounding intervals");
}
#[test]
fn rounding_intervals_does_not_start_at_zero_is_invalid() {
let rounding_intervals = RoundingIntervals {
intervals: vec![
RoundingInterval {
begin_interval: 1,
rounding_mod: 1,
},
RoundingInterval {
begin_interval: 10,
rounding_mod: 1,
},
],
};
rounding_intervals
.validate()
.expect_err("should not accept rounding interval not starting at zero");
}
#[test]
fn rounding_intervals_not_strictly_increasing_is_invalid() {
let rounding_intervals = RoundingIntervals {
intervals: vec![
RoundingInterval {
begin_interval: 0,
rounding_mod: 1,
},
RoundingInterval {
begin_interval: 10,
rounding_mod: 1,
},
RoundingInterval {
begin_interval: 10,
rounding_mod: 1,
},
],
};
rounding_intervals
.validate()
.expect_err("should not accept rounding intervals not strictly increasing");
}
#[test]
fn should_not_round_above_total_collateral() {
let rounding_intervals = RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod: 25,
}],
};
let payout_function = PayoutFunction {
payout_function_pieces: vec![
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 500,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
],
}),
PayoutFunctionPiece::PolynomialPayoutCurvePiece(PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 500,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 1048575,
outcome_payout: Amount::from_sat(7513),
extra_precision: 0,
},
],
}),
],
};
payout_function
.to_range_payouts(Amount::from_sat(75130), &rounding_intervals)
.expect("To be able to compute the range payouts");
}
#[test]
fn floating_point_error_doesnt_fail() {
let function = PayoutFunction::new(vec![PayoutFunctionPiece::PolynomialPayoutCurvePiece(
PolynomialPayoutCurvePiece::new(vec![
PayoutPoint {
event_outcome: 22352,
outcome_payout: Amount::ZERO,
extra_precision: 0,
},
PayoutPoint {
event_outcome: 55881,
outcome_payout: Amount::from_sat(87455),
extra_precision: 0,
},
])
.unwrap(),
)])
.unwrap();
let rounding_mod = 1;
let rounding_intervals = RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
rounding_mod,
}],
};
function
.to_range_payouts(Amount::from_sat(87455), &rounding_intervals)
.expect("Not to fail");
}
#[test]
fn monotonic_increasing_payout_curve_is_valid() {
let polynomial = PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::from_sat(1),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 2,
outcome_payout: Amount::from_sat(5),
extra_precision: 0,
},
],
};
assert_eq!(polynomial.evaluate(0), 1.0);
assert_eq!(polynomial.evaluate(2), 5.0);
}
#[test]
fn monotonic_decreasing_payout_curve_is_valid() {
let polynomial = PolynomialPayoutCurvePiece {
payout_points: vec![
PayoutPoint {
event_outcome: 0,
outcome_payout: Amount::from_sat(10),
extra_precision: 0,
},
PayoutPoint {
event_outcome: 1,
outcome_payout: Amount::from_sat(8),
extra_precision: 0,
},
],
};
assert_eq!(polynomial.evaluate(0), 10.0);
assert_eq!(polynomial.evaluate(1), 8.0);
}
}