Browse Source
490: Depend on cfd protocol from `maia` repository r=klochowicz a=klochowicz Cfd protocol got moved into a separate repository. All references of `cfd_protocol` were renamed to `maia`. Patch cargo.toml with a fixed git revision until it gets a public release. Co-authored-by: Mariusz Klochowicz <mariusz@klochowicz.com>rollover-test
bors[bot]
3 years ago
committed by
GitHub
27 changed files with 47 additions and 3191 deletions
@ -1,8 +1,9 @@ |
|||
[workspace] |
|||
members = ["cfd_protocol", "daemon", "xtra_productivity"] |
|||
members = ["daemon", "xtra_productivity"] |
|||
resolver = "2" |
|||
|
|||
[patch.crates-io] |
|||
rocket = { git = "https://github.com/SergioBenitez/Rocket" } # Need to patch rocket dependency of `rocket_basicauth` until there is an official release. |
|||
xtra = { git = "https://github.com/Restioson/xtra" } # Latest master has crucial patches. |
|||
secp256k1-zkp = { git = "https://github.com/ElementsProject/rust-secp256k1-zkp" } # Latest master has crucial patches. |
|||
maia = { git = "https://github.com/comit-network/maia", rev = "70fc548da0fe4f34478fb34ec437fa9a434c7ee3" } |
|||
|
@ -1,17 +0,0 @@ |
|||
[package] |
|||
name = "cfd_protocol" |
|||
version = "0.1.0" |
|||
edition = "2018" |
|||
|
|||
[dependencies] |
|||
anyhow = "1" |
|||
bdk = { version = "0.13", default-features = false } |
|||
bit-vec = "0.6" |
|||
itertools = "0.10" |
|||
rand = "0.6" |
|||
secp256k1-zkp = { version = "0.4", features = ["bitcoin_hashes", "global-context", "serde"] } |
|||
thiserror = "1" |
|||
|
|||
[dev-dependencies] |
|||
bitcoin = { version = "0.27", features = ["rand", "bitcoinconsensus"] } |
|||
proptest = { version = "1", default-features = false, features = ["std"] } |
@ -1,157 +0,0 @@ |
|||
use bit_vec::BitVec; |
|||
use std::fmt::Display; |
|||
use std::num::NonZeroU8; |
|||
use std::ops::RangeInclusive; |
|||
|
|||
mod digit_decomposition; |
|||
|
|||
/// Maximum supported BTC price in whole USD.
|
|||
pub const MAX_PRICE_DEC: u64 = (BASE as u64).pow(MAX_DIGITS as u32) - 1; |
|||
|
|||
/// Maximum number of binary digits for BTC price in whole USD.
|
|||
const MAX_DIGITS: usize = 20; |
|||
|
|||
const BASE: usize = 2; |
|||
|
|||
/// Binary representation of a price interval.
|
|||
#[derive(Clone, Debug, PartialEq)] |
|||
pub struct Digits(BitVec); |
|||
|
|||
impl Digits { |
|||
pub fn new(range: RangeInclusive<u64>) -> Result<Vec<Self>, Error> { |
|||
let (start, end) = range.into_inner(); |
|||
if start > MAX_PRICE_DEC || end > MAX_PRICE_DEC { |
|||
return Err(Error::RangeOverMax); |
|||
} |
|||
|
|||
if start > end { |
|||
return Err(Error::DecreasingRange); |
|||
} |
|||
|
|||
let digits = digit_decomposition::group_by_ignoring_digits( |
|||
start as usize, |
|||
end as usize, |
|||
BASE, |
|||
MAX_DIGITS, |
|||
) |
|||
.iter() |
|||
.map(|digits| { |
|||
let digits = digits.iter().map(|n| *n != 0).collect::<BitVec>(); |
|||
Digits(digits) |
|||
}) |
|||
.collect(); |
|||
|
|||
Ok(digits) |
|||
} |
|||
|
|||
/// Calculate the range of prices expressed by these digits.
|
|||
///
|
|||
/// With the resulting range one can assess wether a particular
|
|||
/// price corresponds to the described interval.
|
|||
pub fn range(&self) -> RangeInclusive<u64> { |
|||
let missing_bits = MAX_DIGITS - self.0.len(); |
|||
|
|||
let mut bits = self.0.clone(); |
|||
bits.append(&mut BitVec::from_elem(missing_bits, false)); |
|||
let start = bits.as_u64(); |
|||
|
|||
let mut bits = self.0.clone(); |
|||
bits.append(&mut BitVec::from_elem(missing_bits, true)); |
|||
let end = bits.as_u64(); |
|||
|
|||
start..=end |
|||
} |
|||
|
|||
/// Map each bit to its index in the set {0, 1}, starting at 1.
|
|||
pub fn to_indices(&self) -> Vec<NonZeroU8> { |
|||
self.0 |
|||
.iter() |
|||
.map(|bit| NonZeroU8::new(if bit { 2u8 } else { 1u8 }).expect("1 and 2 are non-zero")) |
|||
.collect() |
|||
} |
|||
|
|||
pub fn len(&self) -> usize { |
|||
self.0.len() |
|||
} |
|||
|
|||
pub fn is_empty(&self) -> bool { |
|||
self.0.is_empty() |
|||
} |
|||
} |
|||
|
|||
#[derive(thiserror::Error, Debug)] |
|||
pub enum Error { |
|||
#[error("Interval would generate values over maximum price of {MAX_PRICE_DEC}.")] |
|||
RangeOverMax, |
|||
#[error("Invalid decreasing interval.")] |
|||
DecreasingRange, |
|||
} |
|||
|
|||
impl Display for Digits { |
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|||
self.0 |
|||
.iter() |
|||
.try_for_each(|digit| write!(f, "{}", digit as u8))?; |
|||
|
|||
Ok(()) |
|||
} |
|||
} |
|||
|
|||
trait BitVecExt { |
|||
fn as_u64(&self) -> u64; |
|||
} |
|||
|
|||
impl BitVecExt for BitVec { |
|||
fn as_u64(&self) -> u64 { |
|||
let len = self.len(); |
|||
|
|||
self.iter().enumerate().fold(0, |acc, (i, x)| { |
|||
acc + ((x as u64) * (BASE.pow((len - i - 1) as u32) as u64)) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
use super::*; |
|||
|
|||
use anyhow::Result; |
|||
use proptest::prelude::*; |
|||
|
|||
#[derive(Debug, Clone)] |
|||
struct Interval(RangeInclusive<u64>); |
|||
|
|||
impl Interval { |
|||
fn new(range: RangeInclusive<u64>) -> Self { |
|||
Self(range) |
|||
} |
|||
|
|||
fn to_digits(&self) -> Result<Vec<Digits>> { |
|||
let digits = Digits::new(self.0.clone())?; |
|||
|
|||
Ok(digits) |
|||
} |
|||
} |
|||
|
|||
impl PartialEq<Vec<Digits>> for Interval { |
|||
fn eq(&self, other: &Vec<Digits>) -> bool { |
|||
let sub_intervals = other.iter().flat_map(|i| i.range()); |
|||
sub_intervals.eq(self.0.clone()) |
|||
} |
|||
} |
|||
|
|||
prop_compose! { |
|||
fn interval()(x in 0u64..=MAX_PRICE_DEC, y in 0u64..=MAX_PRICE_DEC) -> Interval { |
|||
let (start, end) = if x < y { (x, y) } else { (y, x) }; |
|||
|
|||
Interval::new(start..=end) |
|||
} |
|||
} |
|||
|
|||
proptest! { |
|||
#[test] |
|||
fn interval_equal_to_sum_of_sub_intervals_described_by_digits(interval in interval()) { |
|||
prop_assert!(interval == interval.to_digits().unwrap()) |
|||
} |
|||
} |
|||
} |
@ -1,459 +0,0 @@ |
|||
//! Utility functions to decompose numeric outcome values
|
|||
//!
|
|||
//! This code has been lifted from:
|
|||
//! <https://github.com/p2pderivatives/rust-dlc/blob/chore%2Ffactor-out-dlc-trie/dlc-trie/src/digit_decomposition.rs>
|
|||
|
|||
/// Describes an interval that starts at `prefix || start` and terminates at `prefix || end`.
|
|||
struct PrefixInterval { |
|||
/// The prefix common to all numbers within the interval.
|
|||
prefix: Vec<usize>, |
|||
/// The suffix of the first number in the interval.
|
|||
start: Vec<usize>, |
|||
/// The suffix of the last number in the interval.
|
|||
end: Vec<usize>, |
|||
} |
|||
|
|||
/// Decompose a numeric value into digits in the specified base. If the decomposed
|
|||
/// value contains less than `nb_digits`, zeroes will be prepended to reach `nb_digits`
|
|||
/// size.
|
|||
fn decompose_value(mut value: usize, base: usize, nb_digits: usize) -> Vec<usize> { |
|||
let mut res = Vec::new(); |
|||
|
|||
while value > 0 { |
|||
res.push(value % base); |
|||
value = ((value as f64) / (base as f64)).floor() as usize; |
|||
} |
|||
|
|||
while res.len() < nb_digits { |
|||
res.push(0); |
|||
} |
|||
|
|||
assert_eq!(nb_digits, res.len()); |
|||
|
|||
res.into_iter().rev().collect() |
|||
} |
|||
|
|||
/// Returns the interval [start, end] as a `PrefixInterval`, which will contain
|
|||
/// the common prefix to all numbers in the interval as well as the start and end
|
|||
/// suffixes decomposed in the specified base, and zero padded to `nb_digits` if
|
|||
/// necessary.
|
|||
fn separate_prefix(start: usize, end: usize, base: usize, nb_digits: usize) -> PrefixInterval { |
|||
let start_digits = decompose_value(start, base, nb_digits); |
|||
let end_digits = decompose_value(end, base, nb_digits); |
|||
let mut prefix = Vec::new(); |
|||
|
|||
let mut i = 0; |
|||
while i < nb_digits && start_digits[i] == end_digits[i] { |
|||
prefix.push(start_digits[i]); |
|||
i += 1; |
|||
} |
|||
let start = start_digits.into_iter().skip(prefix.len()).collect(); |
|||
|
|||
let end = end_digits.into_iter().skip(prefix.len()).collect(); |
|||
|
|||
PrefixInterval { prefix, start, end } |
|||
} |
|||
|
|||
/// Removes the trailing digits from `digits` that are equal to `num`.
|
|||
fn remove_tail_if_equal(mut digits: Vec<usize>, num: usize) -> Vec<usize> { |
|||
let mut i = digits.len(); |
|||
while i > 1 && digits[i - 1] == num { |
|||
i -= 1; |
|||
} |
|||
digits.truncate(i); |
|||
digits |
|||
} |
|||
|
|||
/// Compute the groupings for the end of the interval.
|
|||
fn back_groupings(digits: Vec<usize>, base: usize) -> Vec<Vec<usize>> { |
|||
let digits = remove_tail_if_equal(digits, base - 1); |
|||
if digits.is_empty() { |
|||
return vec![vec![base - 1]]; |
|||
} |
|||
let mut prefix = vec![digits[0]]; |
|||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|||
for digit in digits.iter().skip(1) { |
|||
let mut last = 0; |
|||
let digit = *digit; |
|||
while last < digit { |
|||
let mut new_res = prefix.clone(); |
|||
new_res.push(last); |
|||
res.push(new_res); |
|||
last += 1; |
|||
} |
|||
prefix.push(digit); |
|||
} |
|||
res.push(digits); |
|||
res |
|||
} |
|||
|
|||
/// Compute the groupings for the beginning of the interval.
|
|||
fn front_groupings(digits: Vec<usize>, base: usize) -> Vec<Vec<usize>> { |
|||
let digits = remove_tail_if_equal(digits, 0); |
|||
if digits.is_empty() { |
|||
return vec![vec![0]]; |
|||
} |
|||
let mut prefix = digits.clone(); |
|||
let mut res: Vec<Vec<usize>> = vec![digits.clone()]; |
|||
for digit in digits.into_iter().skip(1).rev() { |
|||
prefix.pop(); |
|||
let mut last = digit + 1; |
|||
while last < base { |
|||
let mut new_res = prefix.clone(); |
|||
new_res.push(last); |
|||
res.push(new_res); |
|||
last += 1; |
|||
} |
|||
} |
|||
|
|||
res |
|||
} |
|||
|
|||
/// Compute the groupings for the middle of the interval.
|
|||
fn middle_grouping(first_digit_start: usize, first_digit_end: usize) -> Vec<Vec<usize>> { |
|||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|||
let mut first_digit_start = first_digit_start + 1; |
|||
while first_digit_start < first_digit_end { |
|||
res.push(vec![first_digit_start]); |
|||
first_digit_start += 1; |
|||
} |
|||
|
|||
res |
|||
} |
|||
|
|||
/// Returns the set of decomposed prefixes that cover the range [start, end].
|
|||
pub(crate) fn group_by_ignoring_digits( |
|||
start: usize, |
|||
end: usize, |
|||
base: usize, |
|||
num_digits: usize, |
|||
) -> Vec<Vec<usize>> { |
|||
let prefix_range = separate_prefix(start, end, base, num_digits); |
|||
let start_is_all_zeros = prefix_range.start.iter().all(|x| *x == 0); |
|||
let end_is_all_max = prefix_range.end.iter().all(|x| *x == base - 1); |
|||
|
|||
if start == end || start_is_all_zeros && end_is_all_max && !prefix_range.prefix.is_empty() { |
|||
return vec![prefix_range.prefix]; |
|||
} |
|||
let mut res: Vec<Vec<usize>> = Vec::new(); |
|||
if prefix_range.prefix.len() == num_digits - 1 { |
|||
for i in prefix_range.start[prefix_range.start.len() - 1] |
|||
..prefix_range.end[prefix_range.end.len() - 1] + 1 |
|||
{ |
|||
let mut new_res = prefix_range.prefix.clone(); |
|||
new_res.push(i); |
|||
res.push(new_res) |
|||
} |
|||
} else { |
|||
let mut front = front_groupings(prefix_range.start.clone(), base); |
|||
let mut middle = middle_grouping(prefix_range.start[0], prefix_range.end[0]); |
|||
let mut back = back_groupings(prefix_range.end.clone(), base); |
|||
res.append(&mut front); |
|||
res.append(&mut middle); |
|||
res.append(&mut back); |
|||
res = res |
|||
.into_iter() |
|||
.map(|x| { |
|||
prefix_range |
|||
.prefix |
|||
.iter() |
|||
.cloned() |
|||
.chain(x.into_iter()) |
|||
.collect() |
|||
}) |
|||
.collect(); |
|||
} |
|||
|
|||
res |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
struct DecompositionTestCase { |
|||
composed: usize, |
|||
decomposed: Vec<usize>, |
|||
base: usize, |
|||
nb_digits: usize, |
|||
} |
|||
|
|||
struct GroupingTestCase { |
|||
start_index: usize, |
|||
end_index: usize, |
|||
base: usize, |
|||
nb_digits: usize, |
|||
expected: Vec<Vec<usize>>, |
|||
} |
|||
fn decomposition_test_cases() -> Vec<DecompositionTestCase> { |
|||
vec![ |
|||
DecompositionTestCase { |
|||
composed: 123456789, |
|||
decomposed: vec![1, 2, 3, 4, 5, 6, 7, 8, 9], |
|||
base: 10, |
|||
nb_digits: 9, |
|||
}, |
|||
DecompositionTestCase { |
|||
composed: 4321, |
|||
decomposed: vec![1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1], |
|||
base: 2, |
|||
nb_digits: 13, |
|||
}, |
|||
DecompositionTestCase { |
|||
composed: 0, |
|||
decomposed: vec![0, 0, 0, 0], |
|||
base: 8, |
|||
nb_digits: 4, |
|||
}, |
|||
DecompositionTestCase { |
|||
composed: 2, |
|||
decomposed: vec![0, 2], |
|||
base: 10, |
|||
nb_digits: 2, |
|||
}, |
|||
DecompositionTestCase { |
|||
composed: 1, |
|||
decomposed: vec![1], |
|||
base: 2, |
|||
nb_digits: 1, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
fn grouping_test_cases() -> Vec<GroupingTestCase> { |
|||
vec![ |
|||
GroupingTestCase { |
|||
start_index: 123, |
|||
end_index: 123, |
|||
base: 10, |
|||
nb_digits: 3, |
|||
expected: vec![vec![1, 2, 3]], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 171, |
|||
end_index: 210, |
|||
base: 16, |
|||
nb_digits: 2, |
|||
expected: vec![ |
|||
vec![10, 11], |
|||
vec![10, 12], |
|||
vec![10, 13], |
|||
vec![10, 14], |
|||
vec![10, 15], |
|||
vec![11], |
|||
vec![12], |
|||
vec![13, 0], |
|||
vec![13, 1], |
|||
vec![13, 2], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 73899, |
|||
end_index: 73938, |
|||
base: 16, |
|||
nb_digits: 6, |
|||
expected: vec![ |
|||
vec![0, 1, 2, 0, 10, 11], |
|||
vec![0, 1, 2, 0, 10, 12], |
|||
vec![0, 1, 2, 0, 10, 13], |
|||
vec![0, 1, 2, 0, 10, 14], |
|||
vec![0, 1, 2, 0, 10, 15], |
|||
vec![0, 1, 2, 0, 11], |
|||
vec![0, 1, 2, 0, 12], |
|||
vec![0, 1, 2, 0, 13, 0], |
|||
vec![0, 1, 2, 0, 13, 1], |
|||
vec![0, 1, 2, 0, 13, 2], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 1234, |
|||
end_index: 4321, |
|||
base: 10, |
|||
nb_digits: 4, |
|||
expected: vec![ |
|||
vec![1, 2, 3, 4], |
|||
vec![1, 2, 3, 5], |
|||
vec![1, 2, 3, 6], |
|||
vec![1, 2, 3, 7], |
|||
vec![1, 2, 3, 8], |
|||
vec![1, 2, 3, 9], |
|||
vec![1, 2, 4], |
|||
vec![1, 2, 5], |
|||
vec![1, 2, 6], |
|||
vec![1, 2, 7], |
|||
vec![1, 2, 8], |
|||
vec![1, 2, 9], |
|||
vec![1, 3], |
|||
vec![1, 4], |
|||
vec![1, 5], |
|||
vec![1, 6], |
|||
vec![1, 7], |
|||
vec![1, 8], |
|||
vec![1, 9], |
|||
vec![2], |
|||
vec![3], |
|||
vec![4, 0], |
|||
vec![4, 1], |
|||
vec![4, 2], |
|||
vec![4, 3, 0], |
|||
vec![4, 3, 1], |
|||
vec![4, 3, 2, 0], |
|||
vec![4, 3, 2, 1], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 1201234, |
|||
end_index: 1204321, |
|||
base: 10, |
|||
nb_digits: 8, |
|||
expected: vec![ |
|||
vec![0, 1, 2, 0, 1, 2, 3, 4], |
|||
vec![0, 1, 2, 0, 1, 2, 3, 5], |
|||
vec![0, 1, 2, 0, 1, 2, 3, 6], |
|||
vec![0, 1, 2, 0, 1, 2, 3, 7], |
|||
vec![0, 1, 2, 0, 1, 2, 3, 8], |
|||
vec![0, 1, 2, 0, 1, 2, 3, 9], |
|||
vec![0, 1, 2, 0, 1, 2, 4], |
|||
vec![0, 1, 2, 0, 1, 2, 5], |
|||
vec![0, 1, 2, 0, 1, 2, 6], |
|||
vec![0, 1, 2, 0, 1, 2, 7], |
|||
vec![0, 1, 2, 0, 1, 2, 8], |
|||
vec![0, 1, 2, 0, 1, 2, 9], |
|||
vec![0, 1, 2, 0, 1, 3], |
|||
vec![0, 1, 2, 0, 1, 4], |
|||
vec![0, 1, 2, 0, 1, 5], |
|||
vec![0, 1, 2, 0, 1, 6], |
|||
vec![0, 1, 2, 0, 1, 7], |
|||
vec![0, 1, 2, 0, 1, 8], |
|||
vec![0, 1, 2, 0, 1, 9], |
|||
vec![0, 1, 2, 0, 2], |
|||
vec![0, 1, 2, 0, 3], |
|||
vec![0, 1, 2, 0, 4, 0], |
|||
vec![0, 1, 2, 0, 4, 1], |
|||
vec![0, 1, 2, 0, 4, 2], |
|||
vec![0, 1, 2, 0, 4, 3, 0], |
|||
vec![0, 1, 2, 0, 4, 3, 1], |
|||
vec![0, 1, 2, 0, 4, 3, 2, 0], |
|||
vec![0, 1, 2, 0, 4, 3, 2, 1], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 2200, |
|||
end_index: 4999, |
|||
base: 10, |
|||
nb_digits: 4, |
|||
expected: vec![ |
|||
vec![2, 2], |
|||
vec![2, 3], |
|||
vec![2, 4], |
|||
vec![2, 5], |
|||
vec![2, 6], |
|||
vec![2, 7], |
|||
vec![2, 8], |
|||
vec![2, 9], |
|||
vec![3], |
|||
vec![4], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 0, |
|||
end_index: 99, |
|||
base: 10, |
|||
nb_digits: 2, |
|||
expected: vec![ |
|||
vec![0], |
|||
vec![1], |
|||
vec![2], |
|||
vec![3], |
|||
vec![4], |
|||
vec![5], |
|||
vec![6], |
|||
vec![7], |
|||
vec![8], |
|||
vec![9], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 100, |
|||
end_index: 199, |
|||
base: 10, |
|||
nb_digits: 3, |
|||
expected: vec![vec![1]], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 100, |
|||
end_index: 200, |
|||
base: 10, |
|||
nb_digits: 3, |
|||
expected: vec![vec![1], vec![2, 0, 0]], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 11, |
|||
end_index: 18, |
|||
base: 10, |
|||
nb_digits: 2, |
|||
expected: vec![ |
|||
vec![1, 1], |
|||
vec![1, 2], |
|||
vec![1, 3], |
|||
vec![1, 4], |
|||
vec![1, 5], |
|||
vec![1, 6], |
|||
vec![1, 7], |
|||
vec![1, 8], |
|||
], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 11, |
|||
end_index: 23, |
|||
base: 2, |
|||
nb_digits: 5, |
|||
expected: vec![vec![0, 1, 0, 1, 1], vec![0, 1, 1], vec![1, 0]], |
|||
}, |
|||
GroupingTestCase { |
|||
start_index: 5677, |
|||
end_index: 8621, |
|||
base: 2, |
|||
nb_digits: 14, |
|||
expected: vec![ |
|||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1], |
|||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1], |
|||
vec![0, 1, 0, 1, 1, 0, 0, 0, 1, 1], |
|||
vec![0, 1, 0, 1, 1, 0, 0, 1], |
|||
vec![0, 1, 0, 1, 1, 0, 1], |
|||
vec![0, 1, 0, 1, 1, 1], |
|||
vec![0, 1, 1], |
|||
vec![1, 0, 0, 0, 0, 0], |
|||
vec![1, 0, 0, 0, 0, 1, 0], |
|||
vec![1, 0, 0, 0, 0, 1, 1, 0, 0], |
|||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0], |
|||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0], |
|||
vec![1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0], |
|||
], |
|||
}, |
|||
] |
|||
} |
|||
|
|||
#[test] |
|||
fn decompose_value_test() { |
|||
for test_case in decomposition_test_cases() { |
|||
assert_eq!( |
|||
test_case.decomposed, |
|||
super::decompose_value(test_case.composed, test_case.base, test_case.nb_digits) |
|||
); |
|||
} |
|||
} |
|||
|
|||
#[test] |
|||
fn group_by_ignoring_digits_test() { |
|||
for test_case in grouping_test_cases() { |
|||
assert_eq!( |
|||
test_case.expected, |
|||
super::group_by_ignoring_digits( |
|||
test_case.start_index, |
|||
test_case.end_index, |
|||
test_case.base, |
|||
test_case.nb_digits |
|||
) |
|||
); |
|||
} |
|||
} |
|||
} |
@ -1,12 +0,0 @@ |
|||
mod oracle; |
|||
mod protocol; |
|||
|
|||
pub mod interval; |
|||
|
|||
pub use protocol::{ |
|||
close_transaction, commit_descriptor, compute_adaptor_pk, create_cfd_transactions, |
|||
finalize_spend_transaction, generate_payouts, lock_descriptor, punish_transaction, |
|||
renew_cfd_transactions, spending_tx_sighash, Announcement, Cets, CfdTransactions, PartyParams, |
|||
Payout, PunishParams, TransactionExt, WalletExt, |
|||
}; |
|||
pub use secp256k1_zkp; |
@ -1,40 +0,0 @@ |
|||
pub use secp256k1_zkp::*; |
|||
|
|||
use anyhow::Result; |
|||
use secp256k1_zkp::schnorrsig; |
|||
use std::num::NonZeroU8; |
|||
|
|||
/// Compute an attestation public key for the given oracle public key,
|
|||
/// announcement nonce public key and outcome index.
|
|||
pub fn attestation_pk( |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
nonce_pk: &schnorrsig::PublicKey, |
|||
index: NonZeroU8, |
|||
) -> Result<secp256k1_zkp::PublicKey> { |
|||
let nonce_pk = schnorr_pubkey_to_pubkey(nonce_pk); |
|||
|
|||
let mut nonce_pk_sum = nonce_pk; |
|||
nonce_pk_sum.mul_assign(SECP256K1, &index_to_bytes(index))?; |
|||
|
|||
let oracle_pk = schnorr_pubkey_to_pubkey(oracle_pk); |
|||
let attestation_pk = oracle_pk.combine(&nonce_pk_sum)?; |
|||
|
|||
Ok(attestation_pk) |
|||
} |
|||
|
|||
fn schnorr_pubkey_to_pubkey(pk: &schnorrsig::PublicKey) -> secp256k1_zkp::PublicKey { |
|||
let mut buf = Vec::<u8>::with_capacity(33); |
|||
|
|||
buf.push(0x02); // append even byte
|
|||
buf.extend(&pk.serialize()); |
|||
|
|||
secp256k1_zkp::PublicKey::from_slice(&buf).expect("valid key") |
|||
} |
|||
|
|||
fn index_to_bytes(index: NonZeroU8) -> [u8; 32] { |
|||
let mut bytes = [0u8; 32]; |
|||
|
|||
bytes[31] = index.get(); |
|||
|
|||
bytes |
|||
} |
@ -1,573 +0,0 @@ |
|||
pub use transaction_ext::TransactionExt; |
|||
pub use transactions::{close_transaction, punish_transaction}; |
|||
|
|||
use crate::protocol::sighash_ext::SigHashExt; |
|||
use crate::protocol::transactions::{ |
|||
lock_transaction, CommitTransaction, ContractExecutionTransaction as ContractExecutionTx, |
|||
RefundTransaction, |
|||
}; |
|||
use crate::{interval, oracle}; |
|||
use anyhow::{bail, Context, Result}; |
|||
use bdk::bitcoin::hashes::hex::ToHex; |
|||
use bdk::bitcoin::util::bip143::SigHashCache; |
|||
use bdk::bitcoin::util::psbt::PartiallySignedTransaction; |
|||
use bdk::bitcoin::{Address, Amount, PublicKey, SigHashType, Transaction, TxOut}; |
|||
use bdk::database::BatchDatabase; |
|||
use bdk::descriptor::Descriptor; |
|||
use bdk::miniscript::descriptor::Wsh; |
|||
use bdk::miniscript::DescriptorTrait; |
|||
use bdk::wallet::AddressIndex; |
|||
use bdk::FeeRate; |
|||
use itertools::Itertools; |
|||
use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, Signature, SECP256K1}; |
|||
use std::collections::HashMap; |
|||
use std::hash::Hasher; |
|||
use std::iter::FromIterator; |
|||
use std::num::NonZeroU8; |
|||
use std::ops::RangeInclusive; |
|||
|
|||
mod sighash_ext; |
|||
mod transaction_ext; |
|||
mod transactions; |
|||
mod txin_ext; |
|||
|
|||
/// Static script to be used to create lock tx
|
|||
const DUMMY_2OF2_MULTISIG: &str = |
|||
"0020b5aa99ed7e0fa92483eb045ab8b7a59146d4d9f6653f21ba729b4331895a5b46"; |
|||
|
|||
pub trait WalletExt { |
|||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams>; |
|||
} |
|||
|
|||
impl<B, D> WalletExt for bdk::Wallet<B, D> |
|||
where |
|||
D: BatchDatabase, |
|||
{ |
|||
fn build_party_params(&self, amount: Amount, identity_pk: PublicKey) -> Result<PartyParams> { |
|||
let mut builder = self.build_tx(); |
|||
builder |
|||
.ordering(bdk::wallet::tx_builder::TxOrdering::Bip69Lexicographic) |
|||
.fee_rate(FeeRate::from_sat_per_vb(1.0)) |
|||
.add_recipient( |
|||
DUMMY_2OF2_MULTISIG.parse().expect("Should be valid script"), |
|||
amount.as_sat(), |
|||
); |
|||
let (lock_psbt, _) = builder.finish()?; |
|||
let address = self.get_address(AddressIndex::New)?.address; |
|||
Ok(PartyParams { |
|||
lock_psbt, |
|||
identity_pk, |
|||
lock_amount: amount, |
|||
address, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
/// Build all the transactions and some of the signatures and
|
|||
/// encrypted signatures needed to perform the CFD protocol.
|
|||
///
|
|||
/// # Arguments
|
|||
///
|
|||
/// * `maker` - The initial parameters of the maker.
|
|||
/// * `maker_punish_params` - The punish parameters of the maker.
|
|||
/// * `taker` - The initial parameters of the taker.
|
|||
/// * `taker_punish_params` - The punish parameters of the taker.
|
|||
/// * `oracle_pk` - The public key of the oracle.
|
|||
/// * `cet_timelock` - Relative timelock of the CET transaction with respect to the commit
|
|||
/// transaction.
|
|||
/// * `refund_timelock` - Relative timelock of the refund transaction with respect to the commit
|
|||
/// transaction.
|
|||
/// * `payouts_per_event` - All the possible ways in which the contract can be settled, according to
|
|||
/// the conditions of the bet. The key is the event at which the oracle will attest the price.
|
|||
/// * `identity_sk` - The secret key of the caller, used to sign and encsign different transactions.
|
|||
pub fn create_cfd_transactions( |
|||
(maker, maker_punish_params): (PartyParams, PunishParams), |
|||
(taker, taker_punish_params): (PartyParams, PunishParams), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
(cet_timelock, refund_timelock): (u32, u32), |
|||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
let lock_tx = lock_transaction( |
|||
maker.lock_psbt.clone(), |
|||
taker.lock_psbt.clone(), |
|||
maker.identity_pk, |
|||
taker.identity_pk, |
|||
maker.lock_amount + taker.lock_amount, |
|||
); |
|||
|
|||
build_cfds( |
|||
lock_tx, |
|||
( |
|||
maker.identity_pk, |
|||
maker.lock_amount, |
|||
maker.address, |
|||
maker_punish_params, |
|||
), |
|||
( |
|||
taker.identity_pk, |
|||
taker.lock_amount, |
|||
taker.address, |
|||
taker_punish_params, |
|||
), |
|||
oracle_pk, |
|||
(cet_timelock, refund_timelock), |
|||
payouts_per_event, |
|||
identity_sk, |
|||
) |
|||
} |
|||
|
|||
pub fn renew_cfd_transactions( |
|||
lock_tx: PartiallySignedTransaction, |
|||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
(cet_timelock, refund_timelock): (u32, u32), |
|||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
build_cfds( |
|||
lock_tx, |
|||
( |
|||
maker_pk, |
|||
maker_lock_amount, |
|||
maker_address, |
|||
maker_punish_params, |
|||
), |
|||
( |
|||
taker_pk, |
|||
taker_lock_amount, |
|||
taker_address, |
|||
taker_punish_params, |
|||
), |
|||
oracle_pk, |
|||
(cet_timelock, refund_timelock), |
|||
payouts_per_event, |
|||
identity_sk, |
|||
) |
|||
} |
|||
|
|||
fn build_cfds( |
|||
lock_tx: PartiallySignedTransaction, |
|||
(maker_pk, maker_lock_amount, maker_address, maker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
(taker_pk, taker_lock_amount, taker_address, taker_punish_params): ( |
|||
PublicKey, |
|||
Amount, |
|||
Address, |
|||
PunishParams, |
|||
), |
|||
oracle_pk: schnorrsig::PublicKey, |
|||
(cet_timelock, refund_timelock): (u32, u32), |
|||
payouts_per_event: HashMap<Announcement, Vec<Payout>>, |
|||
identity_sk: SecretKey, |
|||
) -> Result<CfdTransactions> { |
|||
let commit_tx = CommitTransaction::new( |
|||
&lock_tx.global.unsigned_tx, |
|||
( |
|||
maker_pk, |
|||
maker_punish_params.revocation_pk, |
|||
maker_punish_params.publish_pk, |
|||
), |
|||
( |
|||
taker_pk, |
|||
taker_punish_params.revocation_pk, |
|||
taker_punish_params.publish_pk, |
|||
), |
|||
) |
|||
.context("cannot build commit tx")?; |
|||
|
|||
let identity_pk = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &identity_sk); |
|||
let commit_encsig = if identity_pk == maker_pk.key { |
|||
commit_tx.encsign(identity_sk, &taker_punish_params.publish_pk) |
|||
} else if identity_pk == taker_pk.key { |
|||
commit_tx.encsign(identity_sk, &maker_punish_params.publish_pk) |
|||
} else { |
|||
bail!("identity sk does not belong to taker or maker") |
|||
}; |
|||
|
|||
let refund = { |
|||
let tx = RefundTransaction::new( |
|||
&commit_tx, |
|||
refund_timelock, |
|||
&maker_address, |
|||
&taker_address, |
|||
maker_lock_amount, |
|||
taker_lock_amount, |
|||
); |
|||
|
|||
let sighash = tx.sighash().to_message(); |
|||
let sig = SECP256K1.sign(&sighash, &identity_sk); |
|||
|
|||
(tx.into_inner(), sig) |
|||
}; |
|||
|
|||
let cets = payouts_per_event |
|||
.into_iter() |
|||
.map(|(event, payouts)| { |
|||
let cets = payouts |
|||
.iter() |
|||
.map(|payout| { |
|||
let cet = ContractExecutionTx::new( |
|||
&commit_tx, |
|||
payout.clone(), |
|||
&maker_address, |
|||
&taker_address, |
|||
event.nonce_pks.as_slice(), |
|||
cet_timelock, |
|||
)?; |
|||
|
|||
let encsig = cet.encsign(identity_sk, &oracle_pk)?; |
|||
|
|||
Ok((cet.into_inner(), encsig, payout.digits.clone())) |
|||
}) |
|||
.collect::<Result<Vec<_>>>() |
|||
.context("cannot build and sign all cets")?; |
|||
|
|||
Ok(Cets { event, cets }) |
|||
}) |
|||
.collect::<Result<_>>()?; |
|||
|
|||
Ok(CfdTransactions { |
|||
lock: lock_tx, |
|||
commit: (commit_tx.into_inner(), commit_encsig), |
|||
cets, |
|||
refund, |
|||
}) |
|||
} |
|||
|
|||
pub fn lock_descriptor(maker_pk: PublicKey, taker_pk: PublicKey) -> Descriptor<PublicKey> { |
|||
const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; |
|||
|
|||
let maker_pk = ToHex::to_hex(&maker_pk.key); |
|||
let taker_pk = ToHex::to_hex(&taker_pk.key); |
|||
|
|||
let miniscript = MINISCRIPT_TEMPLATE |
|||
.replace("A", &maker_pk) |
|||
.replace("B", &taker_pk); |
|||
|
|||
let miniscript = miniscript.parse().expect("a valid miniscript"); |
|||
|
|||
Descriptor::Wsh(Wsh::new(miniscript).expect("a valid descriptor")) |
|||
} |
|||
|
|||
pub fn commit_descriptor( |
|||
(maker_own_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
(taker_own_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
) -> Descriptor<PublicKey> { |
|||
let maker_own_pk_hash = maker_own_pk.pubkey_hash().as_hash(); |
|||
let maker_own_pk = (&maker_own_pk.key.serialize().to_vec()).to_hex(); |
|||
let maker_publish_pk_hash = maker_publish_pk.pubkey_hash().as_hash(); |
|||
let maker_rev_pk_hash = maker_rev_pk.pubkey_hash().as_hash(); |
|||
|
|||
let taker_own_pk_hash = taker_own_pk.pubkey_hash().as_hash(); |
|||
let taker_own_pk = (&taker_own_pk.key.serialize().to_vec()).to_hex(); |
|||
let taker_publish_pk_hash = taker_publish_pk.pubkey_hash().as_hash(); |
|||
let taker_rev_pk_hash = taker_rev_pk.pubkey_hash().as_hash(); |
|||
|
|||
// raw script:
|
|||
// or(and(pk(maker_own_pk),pk(taker_own_pk)),or(and(pk(maker_own_pk),and(pk(taker_publish_pk),
|
|||
// pk(taker_rev_pk))),and(pk(taker_own_pk),and(pk(maker_publish_pk),pk(maker_rev_pk)))))
|
|||
let full_script = format!("wsh(c:andor(pk({maker_own_pk}),pk_k({taker_own_pk}),or_i(and_v(v:pkh({maker_own_pk_hash}),and_v(v:pkh({taker_publish_pk_hash}),pk_h({taker_rev_pk_hash}))),and_v(v:pkh({taker_own_pk_hash}),and_v(v:pkh({maker_publish_pk_hash}),pk_h({maker_rev_pk_hash}))))))", |
|||
maker_own_pk = maker_own_pk, |
|||
taker_own_pk = taker_own_pk, |
|||
maker_own_pk_hash = maker_own_pk_hash, |
|||
taker_own_pk_hash = taker_own_pk_hash, |
|||
taker_publish_pk_hash = taker_publish_pk_hash, |
|||
taker_rev_pk_hash = taker_rev_pk_hash, |
|||
maker_publish_pk_hash = maker_publish_pk_hash, |
|||
maker_rev_pk_hash = maker_rev_pk_hash |
|||
); |
|||
|
|||
full_script.parse().expect("a valid miniscript") |
|||
} |
|||
|
|||
pub fn spending_tx_sighash( |
|||
spending_tx: &Transaction, |
|||
spent_descriptor: &Descriptor<PublicKey>, |
|||
spent_amount: Amount, |
|||
) -> secp256k1_zkp::Message { |
|||
let sighash = SigHashCache::new(spending_tx).signature_hash( |
|||
0, |
|||
&spent_descriptor.script_code(), |
|||
spent_amount.as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
sighash.to_message() |
|||
} |
|||
|
|||
pub fn finalize_spend_transaction( |
|||
mut tx: Transaction, |
|||
spent_descriptor: &Descriptor<PublicKey>, |
|||
(pk_0, sig_0): (PublicKey, Signature), |
|||
(pk_1, sig_1): (PublicKey, Signature), |
|||
) -> Result<Transaction> { |
|||
let satisfier = HashMap::from_iter(vec![ |
|||
(pk_0, (sig_0, SigHashType::All)), |
|||
(pk_1, (sig_1, SigHashType::All)), |
|||
]); |
|||
|
|||
let input = tx |
|||
.input |
|||
.iter_mut() |
|||
.exactly_one() |
|||
.expect("all spend transactions to have one input"); |
|||
spent_descriptor.satisfy(input, satisfier)?; |
|||
|
|||
Ok(tx) |
|||
} |
|||
|
|||
#[derive(Clone)] |
|||
pub struct PartyParams { |
|||
pub lock_psbt: PartiallySignedTransaction, |
|||
pub identity_pk: PublicKey, |
|||
pub lock_amount: Amount, |
|||
pub address: Address, |
|||
} |
|||
|
|||
#[derive(Debug, Copy, Clone)] |
|||
pub struct PunishParams { |
|||
pub revocation_pk: PublicKey, |
|||
pub publish_pk: PublicKey, |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub struct CfdTransactions { |
|||
pub lock: PartiallySignedTransaction, |
|||
pub commit: (Transaction, EcdsaAdaptorSignature), |
|||
pub cets: Vec<Cets>, |
|||
pub refund: (Transaction, Signature), |
|||
} |
|||
|
|||
/// Group of CETs associated with a particular oracle announcement.
|
|||
///
|
|||
/// All of the adaptor signatures included will be _possibly_ unlocked
|
|||
/// by the attestation corresponding to the announcement. In practice,
|
|||
/// only one of the adaptor signatures should be unlocked if the
|
|||
/// payout intervals are constructed correctly. To check if an adaptor
|
|||
/// signature can be unlocked by a price attestation, verify whether
|
|||
/// the price attested to lies within its interval.
|
|||
#[derive(Debug, Clone)] |
|||
pub struct Cets { |
|||
pub event: Announcement, |
|||
pub cets: Vec<(Transaction, EcdsaAdaptorSignature, interval::Digits)>, |
|||
} |
|||
|
|||
#[derive(Debug, Clone, Eq)] |
|||
pub struct Announcement { |
|||
pub id: String, |
|||
pub nonce_pks: Vec<schnorrsig::PublicKey>, |
|||
} |
|||
|
|||
impl std::hash::Hash for Announcement { |
|||
fn hash<H: Hasher>(&self, state: &mut H) { |
|||
self.id.hash(state) |
|||
} |
|||
} |
|||
|
|||
impl PartialEq for Announcement { |
|||
fn eq(&self, other: &Self) -> bool { |
|||
self.id.eq(&other.id) |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone, PartialEq)] |
|||
pub struct Payout { |
|||
digits: interval::Digits, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
} |
|||
|
|||
pub fn generate_payouts( |
|||
range: RangeInclusive<u64>, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
) -> Result<Vec<Payout>> { |
|||
let digits = interval::Digits::new(range).context("invalid interval")?; |
|||
Ok(digits |
|||
.into_iter() |
|||
.map(|digits| Payout { |
|||
digits, |
|||
maker_amount, |
|||
taker_amount, |
|||
}) |
|||
.collect()) |
|||
} |
|||
|
|||
impl Payout { |
|||
pub fn digits(&self) -> &interval::Digits { |
|||
&self.digits |
|||
} |
|||
|
|||
pub fn maker_amount(&self) -> &Amount { |
|||
&self.maker_amount |
|||
} |
|||
|
|||
pub fn taker_amount(&self) -> &Amount { |
|||
&self.taker_amount |
|||
} |
|||
|
|||
fn into_txouts(self, maker_address: &Address, taker_address: &Address) -> Vec<TxOut> { |
|||
let txouts = [ |
|||
(self.maker_amount, maker_address), |
|||
(self.taker_amount, taker_address), |
|||
] |
|||
.iter() |
|||
.filter_map(|(amount, address)| { |
|||
let script_pubkey = address.script_pubkey(); |
|||
let dust_limit = script_pubkey.dust_value(); |
|||
(amount >= &dust_limit).then(|| TxOut { |
|||
value: amount.as_sat(), |
|||
script_pubkey, |
|||
}) |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
txouts |
|||
} |
|||
|
|||
/// Subtracts fee fairly from both outputs
|
|||
///
|
|||
/// We need to consider a few cases:
|
|||
/// - If both amounts are >= DUST, they share the fee equally
|
|||
/// - If one amount is < DUST, it set to 0 and the other output needs to cover for the fee.
|
|||
fn with_updated_fee( |
|||
self, |
|||
fee: Amount, |
|||
dust_limit_maker: Amount, |
|||
dust_limit_taker: Amount, |
|||
) -> Result<Self> { |
|||
let maker_amount = self.maker_amount; |
|||
let taker_amount = self.taker_amount; |
|||
|
|||
let mut updated = self; |
|||
match ( |
|||
maker_amount |
|||
.checked_sub(fee / 2) |
|||
.map(|a| a > dust_limit_maker) |
|||
.unwrap_or(false), |
|||
taker_amount |
|||
.checked_sub(fee / 2) |
|||
.map(|a| a > dust_limit_taker) |
|||
.unwrap_or(false), |
|||
) { |
|||
(true, true) => { |
|||
updated.maker_amount -= fee / 2; |
|||
updated.taker_amount -= fee / 2; |
|||
} |
|||
(false, true) => { |
|||
updated.maker_amount = Amount::ZERO; |
|||
updated.taker_amount = taker_amount - (fee + maker_amount); |
|||
} |
|||
(true, false) => { |
|||
updated.maker_amount = maker_amount - (fee + taker_amount); |
|||
updated.taker_amount = Amount::ZERO; |
|||
} |
|||
(false, false) => bail!("Amounts are too small, could not subtract fee."), |
|||
} |
|||
Ok(updated) |
|||
} |
|||
} |
|||
|
|||
pub fn compute_adaptor_pk( |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
index_nonce_pairs: &[(NonZeroU8, schnorrsig::PublicKey)], |
|||
) -> Result<secp256k1_zkp::PublicKey> { |
|||
let attestation_pks = index_nonce_pairs |
|||
.iter() |
|||
.map(|(index, nonce_pk)| oracle::attestation_pk(oracle_pk, nonce_pk, *index)) |
|||
.collect::<Result<Vec<_>>>()?; |
|||
let adaptor_pk = secp256k1_zkp::PublicKey::combine_keys( |
|||
attestation_pks.iter().collect::<Vec<_>>().as_slice(), |
|||
)?; |
|||
|
|||
Ok(adaptor_pk) |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
use super::*; |
|||
|
|||
use bdk::bitcoin::Network; |
|||
|
|||
// TODO add proptest for this
|
|||
|
|||
#[test] |
|||
fn test_fee_subtraction_bigger_than_dust() { |
|||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|||
.parse() |
|||
.unwrap(); |
|||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|||
|
|||
let orig_maker_amount = 1000; |
|||
let orig_taker_amount = 1000; |
|||
let payouts = generate_payouts( |
|||
0..=10_000, |
|||
Amount::from_sat(orig_maker_amount), |
|||
Amount::from_sat(orig_taker_amount), |
|||
) |
|||
.unwrap(); |
|||
let fee = 100; |
|||
|
|||
for payout in payouts { |
|||
let updated_payout = payout |
|||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|||
.unwrap(); |
|||
|
|||
assert_eq!( |
|||
updated_payout.maker_amount, |
|||
Amount::from_sat(orig_maker_amount - fee / 2) |
|||
); |
|||
assert_eq!( |
|||
updated_payout.taker_amount, |
|||
Amount::from_sat(orig_taker_amount - fee / 2) |
|||
); |
|||
} |
|||
} |
|||
|
|||
#[test] |
|||
fn test_fee_subtraction_smaller_than_dust() { |
|||
let key = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af" |
|||
.parse() |
|||
.unwrap(); |
|||
let dummy_address = Address::p2wpkh(&key, Network::Regtest).unwrap(); |
|||
let dummy_dust_limit = dummy_address.script_pubkey().dust_value(); |
|||
|
|||
let orig_maker_amount = dummy_dust_limit.as_sat() - 1; |
|||
let orig_taker_amount = 1000; |
|||
let payouts = generate_payouts( |
|||
0..=10_000, |
|||
Amount::from_sat(orig_maker_amount), |
|||
Amount::from_sat(orig_taker_amount), |
|||
) |
|||
.unwrap(); |
|||
let fee = 100; |
|||
|
|||
for payout in payouts { |
|||
let updated_payout = payout |
|||
.with_updated_fee(Amount::from_sat(fee), dummy_dust_limit, dummy_dust_limit) |
|||
.unwrap(); |
|||
|
|||
assert_eq!(updated_payout.maker_amount, Amount::from_sat(0)); |
|||
assert_eq!( |
|||
updated_payout.taker_amount, |
|||
Amount::from_sat(orig_taker_amount - (fee + orig_maker_amount)) |
|||
); |
|||
} |
|||
} |
|||
} |
@ -1,15 +0,0 @@ |
|||
use bdk::bitcoin::hashes::Hash; |
|||
use bdk::bitcoin::SigHash; |
|||
|
|||
pub(crate) trait SigHashExt { |
|||
fn to_message(self) -> secp256k1_zkp::Message; |
|||
} |
|||
|
|||
impl SigHashExt for SigHash { |
|||
fn to_message(self) -> secp256k1_zkp::Message { |
|||
use secp256k1_zkp::bitcoin_hashes::Hash; |
|||
let hash = secp256k1_zkp::bitcoin_hashes::sha256d::Hash::from_inner(*self.as_inner()); |
|||
|
|||
hash.into() |
|||
} |
|||
} |
@ -1,26 +0,0 @@ |
|||
use anyhow::{Context, Result}; |
|||
use bdk::bitcoin::{OutPoint, Script, Transaction}; |
|||
|
|||
pub trait TransactionExt { |
|||
fn get_virtual_size(&self) -> f64; |
|||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint>; |
|||
} |
|||
|
|||
impl TransactionExt for Transaction { |
|||
fn get_virtual_size(&self) -> f64 { |
|||
self.get_weight() as f64 / 4.0 |
|||
} |
|||
|
|||
fn outpoint(&self, script_pubkey: &Script) -> Result<OutPoint> { |
|||
let vout = self |
|||
.output |
|||
.iter() |
|||
.position(|out| &out.script_pubkey == script_pubkey) |
|||
.context("script pubkey not found in tx")?; |
|||
|
|||
Ok(OutPoint { |
|||
txid: self.txid(), |
|||
vout: vout as u32, |
|||
}) |
|||
} |
|||
} |
@ -1,596 +0,0 @@ |
|||
use crate::protocol::sighash_ext::SigHashExt; |
|||
use crate::protocol::transaction_ext::TransactionExt; |
|||
use crate::protocol::txin_ext::TxInExt; |
|||
use crate::protocol::{ |
|||
commit_descriptor, compute_adaptor_pk, lock_descriptor, Payout, DUMMY_2OF2_MULTISIG, |
|||
}; |
|||
use anyhow::{Context, Result}; |
|||
use bdk::bitcoin::util::bip143::SigHashCache; |
|||
use bdk::bitcoin::util::psbt::{Global, PartiallySignedTransaction}; |
|||
use bdk::bitcoin::{ |
|||
Address, Amount, OutPoint, PublicKey, SigHash, SigHashType, Transaction, TxIn, TxOut, |
|||
}; |
|||
use bdk::descriptor::Descriptor; |
|||
use bdk::miniscript::DescriptorTrait; |
|||
use itertools::Itertools; |
|||
use secp256k1_zkp::{self, schnorrsig, EcdsaAdaptorSignature, SecretKey, SECP256K1}; |
|||
use std::collections::HashMap; |
|||
use std::iter::FromIterator; |
|||
use std::num::NonZeroU8; |
|||
|
|||
/// In satoshi per vbyte.
|
|||
const SATS_PER_VBYTE: f64 = 1.0; |
|||
|
|||
pub(crate) fn lock_transaction( |
|||
maker_psbt: PartiallySignedTransaction, |
|||
taker_psbt: PartiallySignedTransaction, |
|||
maker_pk: PublicKey, |
|||
taker_pk: PublicKey, |
|||
amount: Amount, |
|||
) -> PartiallySignedTransaction { |
|||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|||
|
|||
let maker_change = maker_psbt |
|||
.global |
|||
.unsigned_tx |
|||
.output |
|||
.into_iter() |
|||
.filter(|out| { |
|||
out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
let taker_change = taker_psbt |
|||
.global |
|||
.unsigned_tx |
|||
.output |
|||
.into_iter() |
|||
.filter(|out| { |
|||
out.script_pubkey != DUMMY_2OF2_MULTISIG.parse().expect("To be a valid script") |
|||
}) |
|||
.collect::<Vec<_>>(); |
|||
|
|||
let lock_output = TxOut { |
|||
value: amount.as_sat(), |
|||
script_pubkey: lock_descriptor.script_pubkey(), |
|||
}; |
|||
|
|||
let input = vec![ |
|||
maker_psbt.global.unsigned_tx.input, |
|||
taker_psbt.global.unsigned_tx.input, |
|||
] |
|||
.concat(); |
|||
|
|||
let output = std::iter::once(lock_output) |
|||
.chain(maker_change) |
|||
.chain(taker_change) |
|||
.collect(); |
|||
|
|||
let lock_tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input, |
|||
output, |
|||
}; |
|||
|
|||
PartiallySignedTransaction { |
|||
global: Global::from_unsigned_tx(lock_tx).expect("to be unsigned"), |
|||
inputs: vec![maker_psbt.inputs, taker_psbt.inputs].concat(), |
|||
outputs: vec![maker_psbt.outputs, taker_psbt.outputs].concat(), |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(crate) struct CommitTransaction { |
|||
inner: Transaction, |
|||
descriptor: Descriptor<PublicKey>, |
|||
amount: Amount, |
|||
sighash: SigHash, |
|||
lock_descriptor: Descriptor<PublicKey>, |
|||
fee: Fee, |
|||
} |
|||
|
|||
impl CommitTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 148.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
pub(crate) fn new( |
|||
lock_tx: &Transaction, |
|||
(maker_pk, maker_rev_pk, maker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
(taker_pk, taker_rev_pk, taker_publish_pk): (PublicKey, PublicKey, PublicKey), |
|||
) -> Result<Self> { |
|||
let lock_descriptor = lock_descriptor(maker_pk, taker_pk); |
|||
let (lock_outpoint, lock_amount) = { |
|||
let outpoint = lock_tx |
|||
.outpoint(&lock_descriptor.script_pubkey()) |
|||
.context("lock script not found in lock tx")?; |
|||
let amount = lock_tx.output[outpoint.vout as usize].value; |
|||
|
|||
(outpoint, amount) |
|||
}; |
|||
|
|||
let lock_input = TxIn { |
|||
previous_output: lock_outpoint, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let descriptor = commit_descriptor( |
|||
(maker_pk, maker_rev_pk, maker_publish_pk), |
|||
(taker_pk, taker_rev_pk, taker_publish_pk), |
|||
); |
|||
|
|||
let output = TxOut { |
|||
value: lock_amount, |
|||
script_pubkey: descriptor.script_pubkey(), |
|||
}; |
|||
|
|||
let mut inner = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![lock_input], |
|||
output: vec![output], |
|||
}; |
|||
let fee = Fee::new(Self::SIGNED_VBYTES); |
|||
|
|||
let commit_tx_amount = lock_amount - fee.as_u64(); |
|||
inner.output[0].value = commit_tx_amount; |
|||
|
|||
let sighash = SigHashCache::new(&inner).signature_hash( |
|||
0, |
|||
&lock_descriptor.script_code(), |
|||
lock_amount, |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Ok(Self { |
|||
inner, |
|||
descriptor, |
|||
lock_descriptor, |
|||
amount: Amount::from_sat(commit_tx_amount), |
|||
sighash, |
|||
fee, |
|||
}) |
|||
} |
|||
|
|||
pub(crate) fn encsign( |
|||
&self, |
|||
sk: SecretKey, |
|||
publish_them_pk: &PublicKey, |
|||
) -> EcdsaAdaptorSignature { |
|||
EcdsaAdaptorSignature::encrypt( |
|||
SECP256K1, |
|||
&self.sighash.to_message(), |
|||
&sk, |
|||
&publish_them_pk.key, |
|||
) |
|||
} |
|||
|
|||
pub(crate) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
|
|||
fn outpoint(&self) -> OutPoint { |
|||
self.inner |
|||
.outpoint(&self.descriptor.script_pubkey()) |
|||
.expect("to find commit output in commit tx") |
|||
} |
|||
|
|||
fn amount(&self) -> Amount { |
|||
self.amount |
|||
} |
|||
|
|||
fn descriptor(&self) -> Descriptor<PublicKey> { |
|||
self.descriptor.clone() |
|||
} |
|||
|
|||
fn fee(&self) -> u64 { |
|||
self.fee.as_u64() |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(crate) struct ContractExecutionTransaction { |
|||
inner: Transaction, |
|||
index_nonce_pairs: Vec<(NonZeroU8, schnorrsig::PublicKey)>, |
|||
sighash: SigHash, |
|||
commit_descriptor: Descriptor<PublicKey>, |
|||
} |
|||
|
|||
impl ContractExecutionTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
pub(crate) fn new( |
|||
commit_tx: &CommitTransaction, |
|||
payout: Payout, |
|||
maker_address: &Address, |
|||
taker_address: &Address, |
|||
nonce_pks: &[schnorrsig::PublicKey], |
|||
relative_timelock_in_blocks: u32, |
|||
) -> Result<Self> { |
|||
let index_nonce_pairs: Vec<_> = payout |
|||
.digits |
|||
.to_indices() |
|||
.into_iter() |
|||
.zip(nonce_pks.iter().cloned()) |
|||
.collect(); |
|||
|
|||
let commit_input = TxIn { |
|||
previous_output: commit_tx.outpoint(), |
|||
sequence: relative_timelock_in_blocks, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let fee = Fee::new(Self::SIGNED_VBYTES).add(commit_tx.fee() as f64); |
|||
let output = payout |
|||
.with_updated_fee( |
|||
Amount::from_sat(fee.as_u64()), |
|||
maker_address.script_pubkey().dust_value(), |
|||
taker_address.script_pubkey().dust_value(), |
|||
)? |
|||
.into_txouts(maker_address, taker_address); |
|||
|
|||
let tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![commit_input], |
|||
output, |
|||
}; |
|||
|
|||
let sighash = SigHashCache::new(&tx).signature_hash( |
|||
0, |
|||
&commit_tx.descriptor.script_code(), |
|||
commit_tx.amount.as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Ok(Self { |
|||
inner: tx, |
|||
index_nonce_pairs, |
|||
sighash, |
|||
commit_descriptor: commit_tx.descriptor(), |
|||
}) |
|||
} |
|||
|
|||
pub(crate) fn encsign( |
|||
&self, |
|||
sk: SecretKey, |
|||
oracle_pk: &schnorrsig::PublicKey, |
|||
) -> Result<EcdsaAdaptorSignature> { |
|||
let adaptor_point = compute_adaptor_pk(oracle_pk, &self.index_nonce_pairs)?; |
|||
|
|||
Ok(EcdsaAdaptorSignature::encrypt( |
|||
SECP256K1, |
|||
&self.sighash.to_message(), |
|||
&sk, |
|||
&adaptor_point, |
|||
)) |
|||
} |
|||
|
|||
pub(crate) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
} |
|||
|
|||
#[derive(Debug, Clone)] |
|||
pub(crate) struct RefundTransaction { |
|||
inner: Transaction, |
|||
sighash: SigHash, |
|||
commit_output_descriptor: Descriptor<PublicKey>, |
|||
} |
|||
|
|||
impl RefundTransaction { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 206.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
pub(crate) fn new( |
|||
commit_tx: &CommitTransaction, |
|||
relative_locktime_in_blocks: u32, |
|||
maker_address: &Address, |
|||
taker_address: &Address, |
|||
maker_amount: Amount, |
|||
taker_amount: Amount, |
|||
) -> Self { |
|||
let commit_input = TxIn { |
|||
previous_output: commit_tx.outpoint(), |
|||
sequence: relative_locktime_in_blocks, |
|||
..Default::default() |
|||
}; |
|||
|
|||
let maker_output = TxOut { |
|||
value: maker_amount.as_sat(), |
|||
script_pubkey: maker_address.script_pubkey(), |
|||
}; |
|||
|
|||
let taker_output = TxOut { |
|||
value: taker_amount.as_sat(), |
|||
script_pubkey: taker_address.script_pubkey(), |
|||
}; |
|||
|
|||
let mut tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![commit_input], |
|||
output: vec![maker_output, taker_output], |
|||
}; |
|||
|
|||
let mut fee = Self::SIGNED_VBYTES * SATS_PER_VBYTE; |
|||
fee += commit_tx.fee() as f64; |
|||
tx.output[0].value -= (fee / 2.0) as u64; |
|||
tx.output[1].value -= (fee / 2.0) as u64; |
|||
|
|||
let commit_output_descriptor = commit_tx.descriptor(); |
|||
|
|||
let sighash = SigHashCache::new(&tx).signature_hash( |
|||
0, |
|||
&commit_tx.descriptor().script_code(), |
|||
commit_tx.amount().as_sat(), |
|||
SigHashType::All, |
|||
); |
|||
|
|||
Self { |
|||
inner: tx, |
|||
sighash, |
|||
commit_output_descriptor, |
|||
} |
|||
} |
|||
|
|||
pub(crate) fn sighash(&self) -> SigHash { |
|||
self.sighash |
|||
} |
|||
|
|||
pub(crate) fn into_inner(self) -> Transaction { |
|||
self.inner |
|||
} |
|||
} |
|||
|
|||
/// Build a transaction which closes the CFD contract.
|
|||
///
|
|||
/// This transaction spends directly from the lock transaction. Both
|
|||
/// parties must agree on the split of coins between `maker_amount`
|
|||
/// and `taker_amount`.
|
|||
pub fn close_transaction( |
|||
lock_descriptor: &Descriptor<PublicKey>, |
|||
lock_outpoint: OutPoint, |
|||
lock_amount: Amount, |
|||
(maker_address, maker_amount): (&Address, Amount), |
|||
(taker_address, taker_amount): (&Address, Amount), |
|||
) -> Result<(Transaction, secp256k1_zkp::Message)> { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 167.5 + (3.0 * 2.0) / 4.0; |
|||
|
|||
let lock_input = TxIn { |
|||
previous_output: lock_outpoint, |
|||
..Default::default() |
|||
}; |
|||
|
|||
// TODO: The fee could take into account the network state in this
|
|||
// case, since this transaction is to be broadcast immediately
|
|||
// after building and signing it
|
|||
let (maker_fee, taker_fee) = Fee::new(SIGNED_VBYTES).split(); |
|||
|
|||
let maker_output = TxOut { |
|||
value: maker_amount.as_sat() - maker_fee, |
|||
script_pubkey: maker_address.script_pubkey(), |
|||
}; |
|||
let taker_output = TxOut { |
|||
value: taker_amount.as_sat() - taker_fee, |
|||
script_pubkey: taker_address.script_pubkey(), |
|||
}; |
|||
|
|||
let tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![lock_input], |
|||
output: vec![maker_output, taker_output], |
|||
}; |
|||
|
|||
let sighash = SigHashCache::new(&tx) |
|||
.signature_hash( |
|||
0, |
|||
&lock_descriptor.script_code(), |
|||
lock_amount.as_sat(), |
|||
SigHashType::All, |
|||
) |
|||
.to_message(); |
|||
|
|||
Ok((tx, sighash)) |
|||
} |
|||
|
|||
pub fn punish_transaction( |
|||
commit_descriptor: &Descriptor<PublicKey>, |
|||
address: &Address, |
|||
encsig: EcdsaAdaptorSignature, |
|||
sk: SecretKey, |
|||
revocation_them_sk: SecretKey, |
|||
pub_them_pk: PublicKey, |
|||
revoked_commit_tx: &Transaction, |
|||
) -> Result<Transaction> { |
|||
/// Expected size of signed transaction in virtual bytes, plus a
|
|||
/// buffer to account for different signature lengths.
|
|||
const SIGNED_VBYTES: f64 = 219.5 + (3.0 * 3.0) / 4.0; |
|||
|
|||
let input = revoked_commit_tx |
|||
.input |
|||
.clone() |
|||
.into_iter() |
|||
.exactly_one() |
|||
.context("commit transaction inputs != 1")?; |
|||
|
|||
let publish_them_sk = input |
|||
.find_map_signature(|sig| encsig.recover(SECP256K1, &sig, &pub_them_pk.key).ok()) |
|||
.context("could not recover publish sk from commit tx")?; |
|||
|
|||
let commit_outpoint = revoked_commit_tx |
|||
.outpoint(&commit_descriptor.script_pubkey()) |
|||
.expect("to find commit output in commit tx"); |
|||
let commit_amount = revoked_commit_tx.output[commit_outpoint.vout as usize].value; |
|||
|
|||
let mut punish_tx = { |
|||
let output = TxOut { |
|||
value: commit_amount, |
|||
script_pubkey: address.script_pubkey(), |
|||
}; |
|||
let mut tx = Transaction { |
|||
version: 2, |
|||
lock_time: 0, |
|||
input: vec![TxIn { |
|||
previous_output: commit_outpoint, |
|||
..Default::default() |
|||
}], |
|||
output: vec![output], |
|||
}; |
|||
|
|||
let fee = SIGNED_VBYTES * SATS_PER_VBYTE; |
|||
tx.output[0].value = commit_amount - fee as u64; |
|||
|
|||
tx |
|||
}; |
|||
|
|||
let sighash = SigHashCache::new(&punish_tx).signature_hash( |
|||
0, |
|||
&commit_descriptor.script_code(), |
|||
commit_amount, |
|||
SigHashType::All, |
|||
); |
|||
|
|||
let satisfier = { |
|||
let pk = { |
|||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &sk); |
|||
PublicKey { |
|||
compressed: true, |
|||
key, |
|||
} |
|||
}; |
|||
let pk_hash = pk.pubkey_hash().as_hash(); |
|||
let sig_sk = SECP256K1.sign(&sighash.to_message(), &sk); |
|||
|
|||
let pub_them_pk_hash = pub_them_pk.pubkey_hash().as_hash(); |
|||
let sig_pub_them = SECP256K1.sign(&sighash.to_message(), &publish_them_sk); |
|||
|
|||
let rev_them_pk = { |
|||
let key = secp256k1_zkp::PublicKey::from_secret_key(SECP256K1, &revocation_them_sk); |
|||
PublicKey { |
|||
compressed: true, |
|||
key, |
|||
} |
|||
}; |
|||
let rev_them_pk_hash = rev_them_pk.pubkey_hash().as_hash(); |
|||
let sig_rev_them = SECP256K1.sign(&sighash.to_message(), &revocation_them_sk); |
|||
|
|||
let sighash_all = SigHashType::All; |
|||
HashMap::from_iter(vec![ |
|||
(pk_hash, (pk, (sig_sk, sighash_all))), |
|||
(pub_them_pk_hash, (pub_them_pk, (sig_pub_them, sighash_all))), |
|||
(rev_them_pk_hash, (rev_them_pk, (sig_rev_them, sighash_all))), |
|||
]) |
|||
}; |
|||
|
|||
commit_descriptor.satisfy(&mut punish_tx.input[0], satisfier)?; |
|||
|
|||
Ok(punish_tx) |
|||
} |
|||
|
|||
#[derive(Clone, Debug)] |
|||
struct Fee { |
|||
fee: f64, |
|||
} |
|||
|
|||
impl Fee { |
|||
fn new(signed_vbytes: f64) -> Self { |
|||
let fee = signed_vbytes * SATS_PER_VBYTE; |
|||
Self { fee } |
|||
} |
|||
|
|||
#[must_use] |
|||
fn add(self, number: f64) -> Fee { |
|||
Fee { |
|||
fee: self.fee + number, |
|||
} |
|||
} |
|||
|
|||
fn as_u64(&self) -> u64 { |
|||
// Ceil to prevent going lower than the min relay fee
|
|||
self.fee.ceil() as u64 |
|||
} |
|||
|
|||
fn split(&self) -> (u64, u64) { |
|||
let half = self.as_u64() / 2; |
|||
(half as u64, self.as_u64() - half) |
|||
} |
|||
} |
|||
|
|||
#[cfg(test)] |
|||
mod tests { |
|||
use super::*; |
|||
use proptest::prelude::*; |
|||
|
|||
proptest! { |
|||
#[test] |
|||
fn test_fee_always_above_min_relay_fee(signed_vbytes in 1.0f64..100_000_000.0f64) { |
|||
let fee = Fee::new(signed_vbytes); |
|||
let (maker_fee, taker_fee) = fee.split(); |
|||
|
|||
prop_assert!(signed_vbytes <= fee.as_u64() as f64); |
|||
prop_assert!(signed_vbytes <= (maker_fee + taker_fee) as f64); |
|||
} |
|||
} |
|||
|
|||
// A bunch of tests illustrating how fees are split
|
|||
|
|||
#[test] |
|||
fn test_splitting_fee_1_0() { |
|||
const SIGNED_VBYTES_TEST: f64 = 1.0; |
|||
|
|||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|||
let (maker_fee, taker_fee) = fee.split(); |
|||
|
|||
assert_eq!(fee.as_u64(), 1); |
|||
assert_eq!(maker_fee, 0); |
|||
assert_eq!(taker_fee, 1); |
|||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|||
} |
|||
|
|||
#[test] |
|||
fn test_splitting_fee_2_0() { |
|||
const SIGNED_VBYTES_TEST: f64 = 2.0; |
|||
|
|||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|||
let (maker_fee, taker_fee) = fee.split(); |
|||
|
|||
assert_eq!(fee.as_u64(), 2); |
|||
assert_eq!(maker_fee, 1); |
|||
assert_eq!(taker_fee, 1); |
|||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|||
} |
|||
|
|||
#[test] |
|||
fn test_splitting_fee_2_1() { |
|||
const SIGNED_VBYTES_TEST: f64 = 2.1; |
|||
|
|||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|||
let (maker_fee, taker_fee) = fee.split(); |
|||
|
|||
assert_eq!(fee.as_u64(), 3); |
|||
assert_eq!(maker_fee, 1); |
|||
assert_eq!(taker_fee, 2); |
|||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|||
} |
|||
|
|||
#[test] |
|||
fn test_splitting_fee_2_6() { |
|||
const SIGNED_VBYTES_TEST: f64 = 2.6; |
|||
|
|||
let fee = Fee::new(SIGNED_VBYTES_TEST); |
|||
let (maker_fee, taker_fee) = fee.split(); |
|||
|
|||
assert_eq!(fee.as_u64(), 3); |
|||
assert_eq!(maker_fee, 1); |
|||
assert_eq!(taker_fee, 2); |
|||
assert!((maker_fee + taker_fee) as f64 >= SIGNED_VBYTES_TEST); |
|||
} |
|||
} |
@ -1,23 +0,0 @@ |
|||
use bdk::bitcoin::secp256k1::Signature; |
|||
use bdk::bitcoin::TxIn; |
|||
|
|||
pub(crate) trait TxInExt { |
|||
fn find_map_signature<F, R>(&self, f: F) -> Option<R> |
|||
where |
|||
F: Fn(Signature) -> Option<R>; |
|||
} |
|||
|
|||
impl TxInExt for TxIn { |
|||
fn find_map_signature<F, R>(&self, f: F) -> Option<R> |
|||
where |
|||
F: Fn(Signature) -> Option<R>, |
|||
{ |
|||
self.witness |
|||
.iter() |
|||
.filter_map(|elem| { |
|||
let elem = elem.as_slice(); |
|||
Signature::from_der(&elem[..elem.len() - 1]).ok() |
|||
}) |
|||
.find_map(f) |
|||
} |
|||
} |
File diff suppressed because it is too large
Loading…
Reference in new issue